diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index a73261f1..94d22997 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -6,25 +6,29 @@ "version": "8.0.3", "commands": [ "paket" - ] + ], + "rollForward": false }, "fable": { - "version": "4.9.0", + "version": "4.19.0", "commands": [ "fable" - ] + ], + "rollForward": false }, "femto": { "version": "0.19.0", "commands": [ "femto" - ] + ], + "rollForward": false }, "fantomas": { "version": "6.2.3", "commands": [ "fantomas" - ] + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 5f282702..30fa4c7b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1 +1,12 @@ - \ No newline at end of file +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false + +[*.fs] +fsharp_multiline_bracket_style = stroustrup +fsharp_newline_before_multiline_computation_expression = false \ No newline at end of file diff --git a/.github/workflows/PublishDocker.yaml b/.github/workflows/PublishDocker.yaml index 0c4f50b9..28b78692 100644 --- a/.github/workflows/PublishDocker.yaml +++ b/.github/workflows/PublishDocker.yaml @@ -39,7 +39,7 @@ jobs: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref, event=branch - type=semver,pattern={{version}} + type=semver,pattern={{raw}} # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. diff --git a/build/Build.fs b/build/Build.fs index 499b22c4..d59c3b94 100644 --- a/build/Build.fs +++ b/build/Build.fs @@ -38,7 +38,7 @@ module ReleaseNoteTasks = open Fake.Extensions.Release - let createVersionFile(version: string) = + let createVersionFile(version: string, commit: bool) = let releaseDate = System.DateTime.UtcNow.ToShortDateString() Fake.DotNet.AssemblyInfoFile.createFSharp "src/Server/Version.fs" [ Fake.DotNet.AssemblyInfo.Title "Swate" @@ -46,6 +46,9 @@ module ReleaseNoteTasks = Fake.DotNet.AssemblyInfo.Metadata ("Version",version) Fake.DotNet.AssemblyInfo.Metadata ("ReleaseDate",releaseDate) ] + if commit then + run git ["add"; "."] "" + run git ["commit"; "-m"; (sprintf "Release %s :bookmark:" ProjectInfo.prereleaseTag)] "" let updateReleaseNotes = Target.create "releasenotes" (fun config -> ReleaseNotes.ensure() @@ -53,7 +56,7 @@ module ReleaseNoteTasks = ReleaseNotes.update(ProjectInfo.gitOwner, ProjectInfo.project, config) let newRelease = ReleaseNotes.load "RELEASE_NOTES.md" - createVersionFile(newRelease.AssemblyVersion) + createVersionFile(newRelease.AssemblyVersion, false) Trace.trace "Update Version.fs done!" @@ -223,22 +226,21 @@ module Release = open System.Diagnostics - let private executeCommand (command: string) : string = - let p = new Process() - p.StartInfo.FileName <- "git" - p.StartInfo.Arguments <- command - p.StartInfo.RedirectStandardOutput <- true - p.StartInfo.UseShellExecute <- false - p.StartInfo.CreateNoWindow <- true - - p.Start() |> ignore + let GetLatestGitTag () : string = + let executeCommand (command: string) : string = + let p = new Process() + p.StartInfo.FileName <- "git" + p.StartInfo.Arguments <- command + p.StartInfo.RedirectStandardOutput <- true + p.StartInfo.UseShellExecute <- false + p.StartInfo.CreateNoWindow <- true - let output = p.StandardOutput.ReadToEnd() - p.WaitForExit() + p.Start() |> ignore - output + let output = p.StandardOutput.ReadToEnd() + p.WaitForExit() - let GetLatestGitTag () : string = + output executeCommand "describe --abbrev=0 --tags" |> String.trim @@ -246,7 +248,7 @@ module Release = printfn "Please enter pre-release package suffix" let suffix = System.Console.ReadLine() ProjectInfo.prereleaseSuffix <- suffix - ProjectInfo.prereleaseTag <- (sprintf "%i.%i.%i-%s" ProjectInfo.release.SemVer.Major ProjectInfo.release.SemVer.Minor ProjectInfo.release.SemVer.Patch suffix) + ProjectInfo.prereleaseTag <- (sprintf "v%i.%i.%i-%s" ProjectInfo.release.SemVer.Major ProjectInfo.release.SemVer.Minor ProjectInfo.release.SemVer.Patch suffix) ProjectInfo.isPrerelease <- true let CreateTag() = @@ -258,15 +260,14 @@ module Release = let CreatePrereleaseTag() = if promptYesNo (sprintf "Tagging branch with %s OK?" ProjectInfo.prereleaseTag ) then - Git.Branches.tag "" ProjectInfo.prereleaseTag + run git ["tag"; "-f"; ProjectInfo.prereleaseTag; ] "" Git.Branches.pushTag "" ProjectInfo.projectRepo ProjectInfo.prereleaseTag else failwith "aborted" let ForcePushNightly() = if promptYesNo "Ready to force push release to nightly branch?" then - Git.Commit.exec "." (sprintf "Release v%s" ProjectInfo.prereleaseTag) - run git ["push"; "-f"; "origin"; "HEAD:nightly"] __SOURCE_DIRECTORY__ + run git ["push"; "-f"; "origin"; "HEAD:nightly"] "" else failwith "aborted" @@ -404,7 +405,7 @@ let main args = Release.SetPrereleaseTag() Release.CreatePrereleaseTag() let version = Release.GetLatestGitTag() - ReleaseNoteTasks.createVersionFile(version) + ReleaseNoteTasks.createVersionFile(version, true) Release.ForcePushNightly() 0 | _ -> @@ -420,8 +421,12 @@ let main args = | _ -> runOrDefault args | "version" :: a -> match a with - | "create-file" :: version :: a -> ReleaseNoteTasks.createVersionFile(version); 0 + | "create-file" :: version :: a -> ReleaseNoteTasks.createVersionFile(version, false); 0 | _ -> runOrDefault args + | "cmdtest" :: a -> + run git ["add"; "."] "" + run git ["commit"; "-m"; (sprintf "Release v%s" ProjectInfo.prereleaseTag)] "" + 0 | _ -> runOrDefault args \ No newline at end of file diff --git a/build/Build.fsproj b/build/Build.fsproj index 10a1e12c..b24e0e8f 100644 --- a/build/Build.fsproj +++ b/build/Build.fsproj @@ -15,5 +15,4 @@ - \ No newline at end of file diff --git a/build/manifest.xml b/build/manifest.xml index 969174f7..98a5568c 100644 --- a/build/manifest.xml +++ b/build/manifest.xml @@ -1,4 +1,4 @@ - + 5d6f5462-3401-48ec-9406-d12882e9ad83 1.0.0 @@ -16,7 +16,7 @@ - + ReadWriteDocument @@ -65,7 +65,7 @@ - + @@ -116,8 +116,8 @@ - - + + diff --git a/build/paket.references b/build/paket.references deleted file mode 100644 index 08bfadf2..00000000 --- a/build/paket.references +++ /dev/null @@ -1,3 +0,0 @@ -ARCtrl -FsSpreadsheet.Exceljs -Feliz.Bulma.Checkradio \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 63fd122d..315784ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,3658 +1,4138 @@ { - "name": "Swate", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@creativebulma/bulma-tooltip": "^1.2.0", - "@nfdi4plants/exceljs": "^0.3.0", - "bulma": "^0.9.4", - "bulma-checkradio": "^2.1.3", - "bulma-slider": "^2.0.5", - "bulma-switch": "^2.0.4", - "cytoscape": "^3.27.0", - "human-readable-ids": "^1.0.4", - "isomorphic-fetch": "^3.0.0", - "jsonschema": "^1.4.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "use-sync-external-store": "^1.2.0" - }, - "devDependencies": { - "@types/node": "^20.10.3", - "@vitejs/plugin-basic-ssl": "^1.0.2", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.16", - "core-js": "^3.33.3", - "postcss": "^8.4.32", - "remotedev": "^0.2.9", - "sass": "^1.69.5", - "selfsigned": "^2.4.1", - "tailwindcss": "^3.3.6", - "vite": "^5.0.5" - }, - "engines": { - "node": "~18 || ~20", - "npm": "~9 || ~10" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", - "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.5", - "@babel/parser": "^7.23.5", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", - "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", - "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", - "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", - "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.5", - "@babel/types": "^7.23.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@creativebulma/bulma-tooltip": { - "version": "1.2.0", - "license": "MIT" - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", - "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", - "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", - "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", - "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", - "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", - "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", - "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", - "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", - "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", - "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", - "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", - "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", - "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", - "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", - "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", - "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", - "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", - "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", - "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", - "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", - "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", - "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fast-csv/format": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", - "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", - "dependencies": { - "@types/node": "^14.0.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.isboolean": "^3.0.3", - "lodash.isequal": "^4.5.0", - "lodash.isfunction": "^3.0.9", - "lodash.isnil": "^4.0.0" - } - }, - "node_modules/@fast-csv/format/node_modules/@types/node": { - "version": "14.18.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" - }, - "node_modules/@fast-csv/parse": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", - "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", - "dependencies": { - "@types/node": "^14.0.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.groupby": "^4.6.0", - "lodash.isfunction": "^3.0.9", - "lodash.isnil": "^4.0.0", - "lodash.isundefined": "^3.0.1", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@fast-csv/parse/node_modules/@types/node": { - "version": "14.18.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nfdi4plants/exceljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nfdi4plants/exceljs/-/exceljs-0.3.0.tgz", - "integrity": "sha512-/IvHS3ozGyZ2jG1pYpMoUn2vz+GMzkdo8zUnhsfnn2175ajnjlQKQi7qVhp8Kgpvt/FtthcysrloOjlttbyJQQ==", - "dependencies": { - "archiver": "^5.0.0", - "dayjs": "^1.8.34", - "fast-csv": "^4.3.1", - "jszip": "^3.10.1", - "readable-stream": "^3.6.0", - "saxes": "^5.0.1", - "tmp": "^0.2.0", - "unzipper": "^0.10.11", - "uuid": "^8.3.0" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.1.tgz", - "integrity": "sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.1.tgz", - "integrity": "sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.1.tgz", - "integrity": "sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.1.tgz", - "integrity": "sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.1.tgz", - "integrity": "sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.1.tgz", - "integrity": "sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.1.tgz", - "integrity": "sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.1.tgz", - "integrity": "sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.1.tgz", - "integrity": "sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.1.tgz", - "integrity": "sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.1.tgz", - "integrity": "sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.1.tgz", - "integrity": "sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.7", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", - "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", - "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/node": { - "version": "20.10.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", - "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz", - "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.2.tgz", - "integrity": "sha512-DKHKVtpI+eA5fvObVgQ3QtTGU70CcCnedalzqmGSR050AzKZMdUzgC8KmlOneHWH8dF2hJ3wkC9+8FDVAaDRCw==", - "dev": true, - "engines": { - "node": ">=14.6.0" - }, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", - "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.5", - "@babel/plugin-transform-react-jsx-self": "^7.23.3", - "@babel/plugin-transform-react-jsx-source": "^7.23.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" - } - }, - "node_modules/acorn": { - "version": "8.6.0", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "node_modules/anymatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, - "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" - }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", - "engines": { - "node": ">=0.2.0" - } - }, - "node_modules/bulma": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", - "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==" - }, - "node_modules/bulma-checkradio": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bulma-checkradio/-/bulma-checkradio-2.1.3.tgz", - "integrity": "sha512-8OmZ7PURyftNLGXSTNAYNTJHIe0OkoH/8z9iWfSXGxiv3AlrKneMtiVpBKofXsvc9ZHBUI1YjefiW5WFhgFgAQ==", - "dependencies": { - "bulma": "^0.9.3" - } - }, - "node_modules/bulma-slider": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/bulma-slider/-/bulma-slider-2.0.5.tgz", - "integrity": "sha512-6woD/1E7q1o5bfEaQjNqpWZaCItC1oHe9bN15WYB2ELqz2gDaJYZkf+rlozGpAYOXQGDQGCCv3y+QuKjx6sQuw==" - }, - "node_modules/bulma-switch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.4.tgz", - "integrity": "sha512-kMu4H0Pr0VjvfsnT6viRDCgptUq0Rvy7y7PX6q+IHg1xUynsjszPjhAdal5ysAlCG5HNO+5YXxeiu92qYGQolw==" - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001566", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", - "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha512-h5FLmEMFHeuzqmpVRcDayNlVZ+k4uK1niyKQN6oUMe7ieJihv44Vc3dY/kDnnWX4PDQSwes48s965PG/D4GntQ==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", - "dev": true - }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/core-js": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.3.tgz", - "integrity": "sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw==", - "dev": true, - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cytoscape": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.27.0.tgz", - "integrity": "sha512-pPZJilfX9BxESwujODz5pydeGi+FBrXq1rcaB1mfhFXXFJ9GjE6CNndAk+8jPzoXGD+16LtSS4xlYEIUiW4Abg==", - "dependencies": { - "heap": "^0.2.6", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexer2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.488", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz", - "integrity": "sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ==", - "dev": true - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/esbuild": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", - "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fast-csv": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", - "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", - "dependencies": { - "@fast-csv/format": "4.3.5", - "@fast-csv/parse": "4.3.6" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/heap": { - "version": "0.2.7", - "license": "MIT" - }, - "node_modules/human-readable-ids": { - "version": "1.0.4", - "license": "Apache2", - "dependencies": { - "knuth-shuffle": "^1.0.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/immutable": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/jsan": { - "version": "3.1.13", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonschema": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", - "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/knuth-shuffle": { - "version": "1.0.8", - "license": "(MIT OR Apache-2.0)" - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/linked-list": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", - "integrity": "sha512-Zr4ovrd0ODzF3ut2TWZMdHIxb8iFdJc/P3QM4iCJdlxxGHXo69c9hGIHzLo8/FtuR9E6WUZc5irKhtPUgOKMAg==", - "dev": true - }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, - "node_modules/lodash.groupby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", - "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "node_modules/lodash.isfunction": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", - "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" - }, - "node_modules/lodash.isnil": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", - "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isundefined": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", - "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.4.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/querystring": { - "version": "0.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/remotedev": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/remotedev/-/remotedev-0.2.9.tgz", - "integrity": "sha512-W8dHOv9BcFnetFEd08yNb5O9Hd+zkTFFnf9FRjNCkb4u+JgQ/U152Aw4q83AmY3m34d6KZwhK5ip/Qc331+4vA==", - "dev": true, - "dependencies": { - "jsan": "^3.1.3", - "querystring": "^0.2.0", - "rn-host-detect": "^1.0.1", - "socketcluster-client": "^13.0.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rn-host-detect": { - "version": "1.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.6.1.tgz", - "integrity": "sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.6.1", - "@rollup/rollup-android-arm64": "4.6.1", - "@rollup/rollup-darwin-arm64": "4.6.1", - "@rollup/rollup-darwin-x64": "4.6.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.6.1", - "@rollup/rollup-linux-arm64-gnu": "4.6.1", - "@rollup/rollup-linux-arm64-musl": "4.6.1", - "@rollup/rollup-linux-x64-gnu": "4.6.1", - "@rollup/rollup-linux-x64-musl": "4.6.1", - "@rollup/rollup-win32-arm64-msvc": "4.6.1", - "@rollup/rollup-win32-ia32-msvc": "4.6.1", - "@rollup/rollup-win32-x64-msvc": "4.6.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sc-channel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", - "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", - "dev": true, - "dependencies": { - "component-emitter": "1.2.1" - } - }, - "node_modules/sc-errors": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", - "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", - "dev": true - }, - "node_modules/sc-formatter": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.3.tgz", - "integrity": "sha512-lYI/lTs1u1c0geKElcj+bmEUfcP/HuKg2iDeTijPSjiTNFzN3Cf8Qh6tVd65oi7Qn+2/oD7LP4s6GC13v/9NiQ==", - "dev": true - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dev": true, - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/socketcluster-client": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-13.0.1.tgz", - "integrity": "sha512-hxiE2xz6mgaBlhXbtBa4POgWVEvIcjCoHzf5LTUVhI9IL8V2ltV3Ze8pQsi9egqTjSz4RHPfyrJ7BiETe5Kthw==", - "dev": true, - "dependencies": { - "base-64": "0.1.0", - "clone": "2.1.1", - "component-emitter": "1.2.1", - "linked-list": "0.1.0", - "querystring": "0.2.0", - "sc-channel": "^1.2.0", - "sc-errors": "^1.4.0", - "sc-formatter": "^3.0.1", - "uuid": "3.2.1", - "ws": "5.1.1" - } - }, - "node_modules/socketcluster-client/node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/socketcluster-client/node_modules/uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz", - "integrity": "sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==", - "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.14.2", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", - "engines": { - "node": "*" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/unzipper": { - "version": "0.10.14", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", - "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/unzipper/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/unzipper/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/unzipper/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vite": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.5.tgz", - "integrity": "sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg==", - "dev": true, - "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/ws": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.1.1.tgz", - "integrity": "sha512-bOusvpCb09TOBLbpMKszd45WKC2KPtxiyiHanv+H2DE3Az+1db5a/L7sVJZVDPUC1Br8f0SKRr1KjLpD1U/IAw==", - "dev": true, - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } + "name": "Swate", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@creativebulma/bulma-tooltip": "^1.2.0", + "@nfdi4plants/exceljs": "^0.3.0", + "bulma": "^1.0.1", + "bulma-checkradio": "^2.1.3", + "bulma-slider": "^2.0.5", + "bulma-switch": "^2.0.4", + "cytoscape": "^3.27.0", + "human-readable-ids": "^1.0.4", + "isomorphic-fetch": "^3.0.0", + "jsonschema": "^1.4.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "use-sync-external-store": "^1.2.0" + }, + "devDependencies": { + "@types/node": "^20.10.3", + "@vitejs/plugin-basic-ssl": "^1.0.2", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "core-js": "^3.33.3", + "postcss": "^8.4.32", + "remotedev": "^0.2.7", + "sass": "^1.69.5", + "selfsigned": "^2.4.1", + "tailwindcss": "^3.3.6", + "vite": "^5.0.5" + }, + "engines": { + "node": "~18 || ~20", + "npm": "~9 || ~10" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "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", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nfdi4plants/exceljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nfdi4plants/exceljs/-/exceljs-0.3.0.tgz", + "integrity": "sha512-/IvHS3ozGyZ2jG1pYpMoUn2vz+GMzkdo8zUnhsfnn2175ajnjlQKQi7qVhp8Kgpvt/FtthcysrloOjlttbyJQQ==", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "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", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.5", + "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", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/bulma": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.1.tgz", + "integrity": "sha512-+xv/BIAEQakHkR0QVz+s+RjNqfC53Mx9ZYexyaFNFo9wx5i76HXArNdwW7bccyJxa5mgV/T5DcVGqsAB19nBJQ==" + }, + "node_modules/bulma-checkradio": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bulma-checkradio/-/bulma-checkradio-2.1.3.tgz", + "integrity": "sha512-8OmZ7PURyftNLGXSTNAYNTJHIe0OkoH/8z9iWfSXGxiv3AlrKneMtiVpBKofXsvc9ZHBUI1YjefiW5WFhgFgAQ==", + "dependencies": { + "bulma": "^0.9.3" + } + }, + "node_modules/bulma-checkradio/node_modules/bulma": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", + "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==" + }, + "node_modules/bulma-slider": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/bulma-slider/-/bulma-slider-2.0.5.tgz", + "integrity": "sha512-6woD/1E7q1o5bfEaQjNqpWZaCItC1oHe9bN15WYB2ELqz2gDaJYZkf+rlozGpAYOXQGDQGCCv3y+QuKjx6sQuw==" + }, + "node_modules/bulma-switch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.4.tgz", + "integrity": "sha512-kMu4H0Pr0VjvfsnT6viRDCgptUq0Rvy7y7PX6q+IHg1xUynsjszPjhAdal5ysAlCG5HNO+5YXxeiu92qYGQolw==" + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001638", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001638.tgz", + "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha512-h5FLmEMFHeuzqmpVRcDayNlVZ+k4uK1niyKQN6oUMe7ieJihv44Vc3dY/kDnnWX4PDQSwes48s965PG/D4GntQ==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", + "dev": true + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-js": { + "version": "3.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", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cytoscape": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.0.tgz", + "integrity": "sha512-l590mjTHT6/Cbxp13dGPC2Y7VXdgc+rUeF8AnF/JPzhjNevbDJfObnJgaSjlldOgBQZbue+X6IUZ7r5GAgvauQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.812", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.812.tgz", + "integrity": "sha512-7L8fC2Ey/b6SePDFKR2zHAy4mbdp1/38Yk5TsARO66W3hC5KEaeKMMHoxwtuH+jcu2AYLSn9QX04i95t6Fl1Hg==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-readable-ids": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/human-readable-ids/-/human-readable-ids-1.0.4.tgz", + "integrity": "sha512-h1zwThTims8A/SpqFGWyTx+jG1+WRMJaEeZgbtPGrIpj2AZjsOgy8Y+iNzJ0yAyN669Q6F02EK66WMWcst+2FA==", + "dependencies": { + "knuth-shuffle": "^1.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsan": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/jsan/-/jsan-3.1.14.tgz", + "integrity": "sha512-wStfgOJqMv4QKktuH273f5fyi3D3vy2pHOiSDGPvpcS/q+wb/M7AK3vkCcaHbkZxDOlDU/lDJgccygKSG2OhtA==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonschema": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/knuth-shuffle": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/knuth-shuffle/-/knuth-shuffle-1.0.8.tgz", + "integrity": "sha512-IdC4Hpp+mx53zTt6VAGsAtbGM0g4BV9fP8tTcviCosSwocHcRDw9uG5Rnv6wLWckF4r72qeXFoK9NkvV1gUJCQ==" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/linked-list": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", + "integrity": "sha512-Zr4ovrd0ODzF3ut2TWZMdHIxb8iFdJc/P3QM4iCJdlxxGHXo69c9hGIHzLo8/FtuR9E6WUZc5irKhtPUgOKMAg==", + "dev": true + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/remotedev": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/remotedev/-/remotedev-0.2.9.tgz", + "integrity": "sha512-W8dHOv9BcFnetFEd08yNb5O9Hd+zkTFFnf9FRjNCkb4u+JgQ/U152Aw4q83AmY3m34d6KZwhK5ip/Qc331+4vA==", + "dev": true, + "dependencies": { + "jsan": "^3.1.3", + "querystring": "^0.2.0", + "rn-host-detect": "^1.0.1", + "socketcluster-client": "^13.0.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rn-host-detect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rn-host-detect/-/rn-host-detect-1.2.0.tgz", + "integrity": "sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==", + "dev": true + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sc-channel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", + "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", + "dev": true, + "dependencies": { + "component-emitter": "1.2.1" + } + }, + "node_modules/sc-errors": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", + "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", + "dev": true + }, + "node_modules/sc-formatter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.3.tgz", + "integrity": "sha512-lYI/lTs1u1c0geKElcj+bmEUfcP/HuKg2iDeTijPSjiTNFzN3Cf8Qh6tVd65oi7Qn+2/oD7LP4s6GC13v/9NiQ==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/socketcluster-client": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-13.0.1.tgz", + "integrity": "sha512-hxiE2xz6mgaBlhXbtBa4POgWVEvIcjCoHzf5LTUVhI9IL8V2ltV3Ze8pQsi9egqTjSz4RHPfyrJ7BiETe5Kthw==", + "dev": true, + "dependencies": { + "base-64": "0.1.0", + "clone": "2.1.1", + "component-emitter": "1.2.1", + "linked-list": "0.1.0", + "querystring": "0.2.0", + "sc-channel": "^1.2.0", + "sc-errors": "^1.4.0", + "sc-formatter": "^3.0.1", + "uuid": "3.2.1", + "ws": "5.1.1" + } + }, + "node_modules/socketcluster-client/node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/socketcluster-client/node_modules/uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", + "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.1.1.tgz", + "integrity": "sha512-bOusvpCb09TOBLbpMKszd45WKC2KPtxiyiHanv+H2DE3Az+1db5a/L7sVJZVDPUC1Br8f0SKRr1KjLpD1U/IAw==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } } + } } diff --git a/package.json b/package.json index 5c84c1f9..a48e3a1b 100644 --- a/package.json +++ b/package.json @@ -3,38 +3,38 @@ "ie 11" ], "private": true, - "type": "module", - "engines": { - "node": "~18 || ~20", - "npm": "~9 || ~10" - }, - "scripts": {}, - "devDependencies": { - "@types/node": "^20.10.3", - "@vitejs/plugin-basic-ssl": "^1.0.2", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.16", - "core-js": "^3.33.3", - "postcss": "^8.4.32", - "remotedev": "^0.2.9", - "sass": "^1.69.5", - "selfsigned": "^2.4.1", - "tailwindcss": "^3.3.6", - "vite": "^5.0.5" - }, - "dependencies": { - "@creativebulma/bulma-tooltip": "^1.2.0", - "@nfdi4plants/exceljs": "^0.3.0", - "bulma": "^0.9.4", - "bulma-checkradio": "^2.1.3", - "bulma-slider": "^2.0.5", - "bulma-switch": "^2.0.4", - "cytoscape": "^3.27.0", - "human-readable-ids": "^1.0.4", - "isomorphic-fetch": "^3.0.0", - "jsonschema": "^1.4.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "use-sync-external-store": "^1.2.0" - } + "type": "module", + "engines": { + "node": "~18 || ~20", + "npm": "~9 || ~10" + }, + "scripts": {}, + "devDependencies": { + "@types/node": "^20.10.3", + "@vitejs/plugin-basic-ssl": "^1.0.2", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "core-js": "^3.33.3", + "postcss": "^8.4.32", + "remotedev": "^0.2.7", + "sass": "^1.69.5", + "selfsigned": "^2.4.1", + "tailwindcss": "^3.3.6", + "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", + "bulma-slider": "^2.0.5", + "bulma-switch": "^2.0.4", + "cytoscape": "^3.27.0", + "human-readable-ids": "^1.0.4", + "isomorphic-fetch": "^3.0.0", + "jsonschema": "^1.4.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "use-sync-external-store": "^1.2.0" + } } diff --git a/paket.dependencies b/paket.dependencies index 7ef8ff1e..5b76225c 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -2,12 +2,19 @@ source https://api.nuget.org/v3/index.json framework: net8.0 storage: none -nuget ARCtrl 1.0.5 +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.Exceljs ~> 5.0.2 +nuget FsSpreadsheet.Js ~> 6.1.3 nuget Saturn ~> 0 nuget Fable.Core ~> 4 diff --git a/paket.lock b/paket.lock index f622e315..1ceec7ed 100644 --- a/paket.lock +++ b/paket.lock @@ -2,38 +2,41 @@ STORAGE: NONE RESTRICTION: == net8.0 NUGET remote: https://api.nuget.org/v3/index.json - ARCtrl (1.0.5) - ARCtrl.Contract (>= 1.0.5) - ARCtrl.CWL (>= 1.0.5) - ARCtrl.FileSystem (>= 1.0.5) - ARCtrl.ISA (>= 1.0.5) - ARCtrl.ISA.Json (>= 1.0.5) - ARCtrl.ISA.Spreadsheet (>= 1.0.5) + ARCtrl (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.Contract (1.0.5) - ARCtrl.ISA (>= 1.0.5) - FSharp.Core (>= 6.0.7) - ARCtrl.CWL (1.0.5) - FSharp.Core (>= 6.0.7) - ARCtrl.FileSystem (1.0.5) + ARCtrl.FileSystem (2.0.0-alpha.7.swate) Fable.Core (>= 4.2) FSharp.Core (>= 6.0.7) - ARCtrl.ISA (1.0.5) - ARCtrl.FileSystem (>= 1.0.5) - FSharp.Core (>= 6.0.7) - ARCtrl.ISA.Json (1.0.5) - ARCtrl.ISA (>= 1.0.5) - FSharp.Core (>= 6.0.7) + 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 (>= 10.1) - Thoth.Json.Net (>= 11.0) - ARCtrl.ISA.Spreadsheet (1.0.5) - ARCtrl.FileSystem (>= 1.0.5) - ARCtrl.ISA (>= 1.0.5) - FSharp.Core (>= 6.0.7) - FsSpreadsheet (>= 5.0.1) + 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) @@ -41,38 +44,38 @@ NUGET Expecto (9.0.4) FSharp.Core (>= 4.6) Mono.Cecil (>= 0.11.3) - Fable.AST (4.3) - Fable.Browser.Blob (1.3) - Fable.Core (>= 3.0) + Fable.AST (4.5) + Fable.Browser.Blob (1.4) + Fable.Core (>= 3.2.8) FSharp.Core (>= 4.7.2) - Fable.Browser.Dom (2.15) + 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.5) - Fable.Browser.Gamepad (>= 1.1) - Fable.Core (>= 3.0) + Fable.Browser.Event (1.6) + Fable.Browser.Gamepad (>= 1.3) + Fable.Core (>= 3.2.8) FSharp.Core (>= 4.7.2) - Fable.Browser.Gamepad (1.2) - Fable.Core (>= 3.0) + Fable.Browser.Gamepad (1.3) + Fable.Core (>= 3.2.8) FSharp.Core (>= 4.7.2) - Fable.Browser.MediaQueryList (1.4) - Fable.Browser.Dom (>= 2.11) - Fable.Browser.Event (>= 1.5) - Fable.Core (>= 3.0) + 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.2) - Fable.Browser.Event (>= 1.5) - Fable.Core (>= 3.0) + Fable.Browser.WebStorage (1.3) + Fable.Browser.Event (>= 1.6) + Fable.Core (>= 3.2.8) FSharp.Core (>= 4.7.2) - Fable.Browser.XMLHttpRequest (1.3) - Fable.Browser.Blob (>= 1.3) - Fable.Browser.Event (>= 1.5) - Fable.Core (>= 3.0) + 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.2) + Fable.Core (4.3) Fable.Elmish (4.1) Fable.Core (>= 3.7.1) FSharp.Core (>= 4.7.2) @@ -98,7 +101,7 @@ NUGET Fable.Exceljs (1.6) Fable.Core (>= 4.0) FSharp.Core (>= 6.0.7) - Fable.Fetch (2.6) + Fable.Fetch (2.7) Fable.Browser.Blob (>= 1.2) Fable.Browser.Event (>= 1.5) Fable.Core (>= 3.7.1) @@ -113,7 +116,7 @@ NUGET Fable.Promise (3.2) Fable.Core (>= 3.7.1) FSharp.Core (>= 4.7.2) - Fable.React (9.3) + Fable.React (9.4) Fable.React.Types (>= 18.3) Fable.ReactDom.Types (>= 18.2) FSharp.Core (>= 4.7.2) @@ -124,25 +127,25 @@ NUGET Fable.ReactDom.Types (18.2) Fable.React.Types (>= 18.3) FSharp.Core (>= 4.7.2) - Fable.Remoting.Client (7.30) + Fable.Remoting.Client (7.32) Fable.Browser.XMLHttpRequest (>= 1.0) Fable.Core (>= 3.1.5) - Fable.Remoting.MsgPack (>= 1.21) + Fable.Remoting.MsgPack (>= 1.24) Fable.SimpleJson (>= 3.24) FSharp.Core (>= 4.7.2) - Fable.Remoting.Giraffe (5.18) - Fable.Remoting.Server (>= 5.36) + 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.21) + Fable.Remoting.MsgPack (1.24) FSharp.Core (>= 4.7.2) - Fable.Remoting.Server (5.36) + Fable.Remoting.Server (5.37) Fable.Remoting.Json (>= 2.23) - Fable.Remoting.MsgPack (>= 1.21) + Fable.Remoting.MsgPack (>= 1.24) FSharp.Core (>= 6.0) Microsoft.IO.RecyclableMemoryStream (>= 3.0 < 4.0) Fable.SimpleHttp (3.6) @@ -182,27 +185,28 @@ NUGET Feliz.UseElmish (2.5) Fable.Elmish (>= 4.0) FSharp.Core (>= 4.7.2) - FSharp.Control.Websockets (0.2.3) + FSharp.Control.Websockets (0.3) FSharp.Core (>= 6.0) - Microsoft.IO.RecyclableMemoryStream (>= 2.2.1) - FSharp.Core (8.0.101) - FsSpreadsheet (5.0.2) - Fable.Core (>= 4.0) + Microsoft.IO.RecyclableMemoryStream (>= 3.0) + FSharp.Core (8.0.300) + FsSpreadsheet (6.1.3) FSharp.Core (>= 6.0.7) - FsSpreadsheet.Exceljs (5.0.2) + 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 (>= 5.0.2) - Giraffe (6.2) + FsSpreadsheet (>= 6.1.3) + Thoth.Json.JavaScript (>= 0.1) + Giraffe (6.4) FSharp.Core (>= 6.0) Giraffe.ViewEngine (>= 1.4) - Microsoft.IO.RecyclableMemoryStream (>= 2.2.1) + Microsoft.IO.RecyclableMemoryStream (>= 3.0) Newtonsoft.Json (>= 13.0.3) - System.Text.Json (>= 7.0.3) + System.Text.Json (>= 8.0.3) Giraffe.ViewEngine (1.4) FSharp.Core (>= 5.0) - Microsoft.AspNetCore.Authentication.JwtBearer (6.0.26) + Microsoft.AspNetCore.Authentication.JwtBearer (6.0.31) Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 6.35) Microsoft.Bcl.AsyncInterfaces (8.0) Microsoft.CSharp (4.7) @@ -236,24 +240,23 @@ NUGET Microsoft.Extensions.Primitives (>= 8.0) Microsoft.Extensions.FileSystemGlobbing (8.0) Microsoft.Extensions.Primitives (8.0) - Microsoft.IdentityModel.Abstractions (7.2) - Microsoft.IdentityModel.JsonWebTokens (7.2) - Microsoft.IdentityModel.Tokens (>= 7.2) - Microsoft.IdentityModel.Logging (7.2) - Microsoft.IdentityModel.Abstractions (>= 7.2) - Microsoft.IdentityModel.Protocols (7.2) - Microsoft.IdentityModel.Logging (>= 7.2) - Microsoft.IdentityModel.Tokens (>= 7.2) - Microsoft.IdentityModel.Protocols.OpenIdConnect (7.2) - Microsoft.IdentityModel.Protocols (>= 7.2) - System.IdentityModel.Tokens.Jwt (>= 7.2) - Microsoft.IdentityModel.Tokens (7.2) - Microsoft.IdentityModel.Logging (>= 7.2) - Microsoft.IO.RecyclableMemoryStream (3.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.16) + Neo4j.Driver (5.21) Microsoft.Bcl.AsyncInterfaces (>= 5.0) System.IO.Pipelines (>= 7.0) System.ValueTuple (>= 4.5) @@ -263,19 +266,19 @@ NUGET Newtonsoft.Json (>= 13.0.3) NJsonSchema.Annotations (>= 11.0) NJsonSchema.Annotations (11.0) - Saturn (0.16.1) + Saturn (0.17) FSharp.Control.Websockets (>= 0.2.2) - Giraffe (>= 6.0) + Giraffe (>= 6.4) Microsoft.AspNetCore.Authentication.JwtBearer (>= 6.0.3) - System.IdentityModel.Tokens.Jwt (7.2) - Microsoft.IdentityModel.JsonWebTokens (>= 7.2) - Microsoft.IdentityModel.Tokens (>= 7.2) + 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.1) + System.Text.Json (8.0.3) System.Text.Encodings.Web (>= 8.0) System.ValueTuple (4.5) - Thoth.Elmish.Debouncer (2.0) + Thoth.Elmish.Debouncer (2.1) Fable.Core (>= 4.0) Fable.Elmish (>= 4.0.1) Fable.Promise (>= 3.2) @@ -284,7 +287,15 @@ NUGET Thoth.Json (10.2) Fable.Core (>= 3.6.2) FSharp.Core (>= 4.7.2) - Thoth.Json.Net (11.0) - Fable.Core (>= 3.1.6) - FSharp.Core (>= 4.7.2) - Newtonsoft.Json (>= 11.0.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/ARCitect/ARCitect.fs b/src/Client/ARCitect/ARCitect.fs index fd09db01..fa1f86c8 100644 --- a/src/Client/ARCitect/ARCitect.fs +++ b/src/Client/ARCitect/ARCitect.fs @@ -6,22 +6,25 @@ open Model.ARCitect open Shared open Messages open Elmish -open ARCtrl.ISA -open ARCtrl.ISA.Json +open ARCtrl +open ARCtrl.Json let send (msg:ARCitect.Msg) = let (data: obj) = match msg with | Init -> "Hello from Swate!" - | TriggerSwateClose -> - null | AssayToARCitect assay -> - let assay = ArcAssay.toArcJsonString assay + let assay = ArcAssay.toJsonString 0 assay assay | StudyToARCitect study -> - let json = ArcStudy.toArcJsonString study + let json = ArcStudy.toJsonString 0 study json + | InvestigationToARCitect inv -> + let json = ArcInvestigation.toJsonString 0 inv + json + | RequestPaths selectDirectories -> + selectDirectories | Error exn -> exn postMessageToARCitect(msg, data) @@ -29,14 +32,20 @@ let send (msg:ARCitect.Msg) = let EventHandler (dispatch: Messages.Msg -> unit) : IEventHandler = { AssayToSwate = fun data -> - let assay = ArcAssay.fromArcJsonString data.ArcAssayJsonString + let assay = ArcAssay.fromJsonString data.ArcAssayJsonString log($"Received Assay {assay.Identifier} from ARCitect!") Spreadsheet.InitFromArcFile (ArcFiles.Assay assay) |> SpreadsheetMsg |> dispatch StudyToSwate = fun data -> - let study = ArcStudy.fromArcJsonString data.ArcStudyJsonString + let study = ArcStudy.fromJsonString data.ArcStudyJsonString Spreadsheet.InitFromArcFile (ArcFiles.Study (study, [])) |> SpreadsheetMsg |> dispatch log($"Received Study {study.Identifier} from ARCitect!") - Browser.Dom.console.log(study) + InvestigationToSwate = fun data -> + let inv = ArcInvestigation.fromJsonString data.ArcInvestigationJsonString + Spreadsheet.InitFromArcFile (ArcFiles.Investigation inv) |> SpreadsheetMsg |> dispatch + log($"Received Investigation {inv.Title} from ARCitect!") + PathsToSwate = fun paths -> + log $"Received {paths.paths.Length} paths from ARCitect!" + FilePicker.LoadNewFiles (List.ofArray paths.paths) |> FilePickerMsg |> dispatch Error = fun exn -> GenericError (Cmd.none, exn) |> DevMsg |> dispatch } \ No newline at end of file diff --git a/src/Client/ARCitect/Interop.fs b/src/Client/ARCitect/Interop.fs index 2331a275..2f496733 100644 --- a/src/Client/ARCitect/Interop.fs +++ b/src/Client/ARCitect/Interop.fs @@ -45,6 +45,7 @@ let initEventListener (eventHandler: IEventHandler) : unit -> unit = let e = e :?> Browser.Types.MessageEvent match verifyARCitectMsg e with | Some content -> + log ("Message from ARCitect: " + content.api) runApiFromName eventHandler content.api content.data | None -> () diff --git a/src/Client/Client.fable-temp.csproj b/src/Client/Client.fable-temp.csproj new file mode 100644 index 00000000..9f7bb983 --- /dev/null +++ b/src/Client/Client.fable-temp.csproj @@ -0,0 +1,123 @@ + + + + + net8.0 + FABLE_COMPILER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Client/Client.fs b/src/Client/Client.fs index f6bb79bc..a3092a20 100644 --- a/src/Client/Client.fs +++ b/src/Client/Client.fs @@ -15,10 +15,11 @@ let _ = importSideEffects "./style.scss" let sayHello name = $"Hello {name}" open Feliz +open Feliz.Bulma let private split_container model dispatch = - let mainWindow = Seq.singleton <| MainWindowView.Main model dispatch - let sideWindow = Seq.singleton <| SidebarView.SidebarView model dispatch + let mainWindow = Seq.singleton <| MainWindowView.Main (model, dispatch) + let sideWindow = Seq.singleton <| SidebarView.SidebarView.Main(model, dispatch) SplitWindowView.Main mainWindow sideWindow @@ -28,13 +29,18 @@ let private split_container model dispatch = [] let View (model : Model) (dispatch : Msg -> unit) = let (colorstate, setColorstate) = React.useState(LocalStorage.Darkmode.State.init) + // Make ARCitect always use lighttheme + let makeColorSchemeLight = fun _ -> + if model.PersistentStorageState.Host.IsSome && model.PersistentStorageState.Host.Value = Swatehost.ARCitect then + setColorstate {colorstate with Theme = LocalStorage.Darkmode.DataTheme.Light} + LocalStorage.Darkmode.DataTheme.SET LocalStorage.Darkmode.DataTheme.Light + React.useEffect(makeColorSchemeLight, [|box model.PersistentStorageState.Host|]) let v = {colorstate with SetTheme = setColorstate} React.contextProvider(LocalStorage.Darkmode.themeContext, v, Html.div [ - Html.div [prop.id "modal-container"] match model.PersistentStorageState.Host with | Some Swatehost.Excel -> - SidebarView.SidebarView model dispatch + SidebarView.SidebarView.Main(model, dispatch) | _ -> split_container model dispatch ] diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 46d78fe7..02614367 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -6,6 +6,7 @@ + @@ -13,6 +14,7 @@ + @@ -24,21 +26,17 @@ - - + - - - @@ -53,28 +51,18 @@ + + + - + - - - - - - - - - - - - - @@ -89,15 +77,31 @@ - + + + - - + + + + + + + + + + + + + + + + diff --git a/src/Client/Helper.fs b/src/Client/Helper.fs index 9803ffb3..5e43dcb5 100644 --- a/src/Client/Helper.fs +++ b/src/Client/Helper.fs @@ -15,8 +15,8 @@ let debounce<'T> (storage:Dictionary) (key: string) (timeout: int) let key = key // fn.ToString() // Cancel previous debouncer match storage.TryGetValue(key) with - | true, timeoutId -> printfn "CLEAR"; Fable.Core.JS.clearTimeout timeoutId - | _ -> printfn "Not clear";() + | true, timeoutId -> Fable.Core.JS.clearTimeout timeoutId + | _ -> () // Create a new timeout and memoize it let timeoutId = @@ -50,4 +50,33 @@ let debouncel<'T> (storage:Dictionary) (key: string) (timeout: int) timeout storage.[key] <- timeoutId -let newDebounceStorage = fun () -> Dictionary(HashIdentity.Structural) \ No newline at end of file +let newDebounceStorage = fun () -> Dictionary(HashIdentity.Structural) + +type Clipboard = + abstract member writeText: string -> JS.Promise + abstract member readText: unit -> JS.Promise + +type Navigator = + abstract member clipboard: Clipboard + +[] +let navigator : Navigator = jsNative + +/// +/// take "count" many items from array if existing. if not enough items return as many as possible +/// +/// +/// +let takeFromArray (count: int) (array: 'a []) = + let exit (acc: 'a list) = List.rev acc |> Array.ofList + let rec takeRec (l2: 'a list) (acc: 'a list) index = + if index >= count then + exit acc + else + match l2 with + | [] -> exit acc + | item::tail -> + let newAcc = item::acc + takeRec tail newAcc (index+1) + + takeRec (Array.toList array) [] 0 \ No newline at end of file diff --git a/src/Client/Host.fs b/src/Client/Host.fs index 3d84d091..33fd146f 100644 --- a/src/Client/Host.fs +++ b/src/Client/Host.fs @@ -5,11 +5,13 @@ module Host type Swatehost = | Browser | Excel -| ARCitect //WIP - +| ARCitect with static member ofQueryParam (queryInteger: int option) = match queryInteger with | Some 1 -> Swatehost.ARCitect | Some 2 -> Swatehost.Excel - | _ -> Browser \ No newline at end of file + | _ -> Browser + + member this.IsStandalone = + this = Swatehost.Browser || this = Swatehost.ARCitect \ No newline at end of file diff --git a/src/Client/Init.fs b/src/Client/Init.fs index 99e233b2..a55faf06 100644 --- a/src/Client/Init.fs +++ b/src/Client/Init.fs @@ -12,22 +12,15 @@ let initializeModel () = let dt = LocalStorage.Darkmode.DataTheme.GET() LocalStorage.Darkmode.DataTheme.SET dt { - DebouncerState = Debouncer .create () PageState = PageState .init () PersistentStorageState = PersistentStorageState .init () DevState = DevState .init () TermSearchState = TermSearch.Model .init () ExcelState = OfficeInterop.Model .init () - ApiState = ApiState .init () FilePickerState = FilePicker.Model .init () AddBuildingBlockState = BuildingBlock.Model .init () - ValidationState = Validation.Model .init () ProtocolState = Protocol.Model .init () BuildingBlockDetailsState = BuildingBlockDetailsState .init () - SettingsXmlState = SettingsXml.Model .init () - JsonExporterModel = JsonExporter.Model .init () - TemplateMetadataModel = TemplateMetadata.Model .init () - DagModel = Dag.Model .init () CytoscapeModel = Cytoscape.Model .init () SpreadsheetModel = Spreadsheet.Model .fromLocalStorage() History = LocalHistory.Model .init().UpdateFromSessionStorage() diff --git a/src/Client/LocalStorage/Widgets.fs b/src/Client/LocalStorage/Widgets.fs new file mode 100644 index 00000000..1495a4cd --- /dev/null +++ b/src/Client/LocalStorage/Widgets.fs @@ -0,0 +1,68 @@ +module LocalStorage.Widgets + +open Feliz +open Fable.Core.JsInterop + +/// +/// Is not only used to store position but also size. +/// +type Rect = { + X: int + Y: int +} with + static member init () = { + X = 0 + Y = 0 + } + +open Fable.SimpleJson + +let [] BuildingBlockWidgets = "BuildingBlock" +let [] TemplatesWidgets = "Templates" +let [] FilePickerWidgets = "FilerPicker" + +[] +module Position = + + open Browser + + let [] private Key_Prefix = "WidgetsPosition_" + + let write(modalName:string, dt: Rect) = + let s = Json.serialize dt + WebStorage.localStorage.setItem(Key_Prefix + modalName, s) + + let load(modalName:string) = + let key = Key_Prefix + modalName + try + WebStorage.localStorage.getItem(key) + |> Json.parseAs + |> Some + with + |_ -> + WebStorage.localStorage.removeItem(key) + printfn "Could not find %s" key + None + + +[] +module Size = + open Browser + + let [] private Key_Prefix = "WidgetsSize_" + + let write(modalName:string, dt: Rect) = + let s = Json.serialize dt + WebStorage.localStorage.setItem(Key_Prefix + modalName, s) + + let load(modalName:string) = + let key = Key_Prefix + modalName + try + WebStorage.localStorage.getItem(key) + |> Json.parseAs + |> Some + with + |_ -> + WebStorage.localStorage.removeItem(key) + printfn "Could not find %s" key + None diff --git a/src/Client/MainComponents/AddRows.fs b/src/Client/MainComponents/AddRows.fs index 07857199..6ad69ce6 100644 --- a/src/Client/MainComponents/AddRows.fs +++ b/src/Client/MainComponents/AddRows.fs @@ -12,7 +12,6 @@ let Main (dispatch: Messages.Msg -> unit) = let state_rows, setState_rows = React.useState(init_RowsToAdd) Html.div [ prop.id "ExpandTable" - prop.title "Add rows" prop.style [ style.flexGrow 1; style.justifyContent.center; style.display.inheritFromParent; style.padding(length.rem 1) style.position.sticky; style.left 0 @@ -25,11 +24,13 @@ let Main (dispatch: Messages.Msg -> unit) = prop.id "n_row_input" prop.min init_RowsToAdd prop.onChange(fun e -> setState_rows e) + prop.onKeyDown(key.enter, fun _ -> Spreadsheet.AddRows state_rows |> SpreadsheetMsg |> dispatch) prop.defaultValue init_RowsToAdd - prop.style [style.width(50)] + prop.style [style.width(100)] ] Bulma.button.a [ Bulma.button.isRounded + prop.title "Add rows" prop.onClick(fun _ -> let inp = Browser.Dom.document.getElementById "n_row_input" inp?Value <- init_RowsToAdd diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 6753f765..28dc6627 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -7,97 +7,114 @@ open Spreadsheet open MainComponents open Messages open Shared -open ARCtrl.ISA +open ARCtrl +open Components -type private CellState = { - Active: bool - /// This value is used to show during input cell editing. After confirming edit it will be used to push update - Value: string -} with - static member init() = - { - Active = false - Value = "" - } - static member init(v: string) = - { - Active = false - Value = v - } +module private CellComponents = -let private cellStyle (specificStyle: IStyleAttribute list) = prop.style [ - style.minWidth 100 - style.height 22 - style.border(length.px 1, borderStyle.solid, "darkgrey") - yield! specificStyle - ] - -let private cellInnerContainerStyle (specificStyle: IStyleAttribute list) = prop.style [ - style.display.flex; - style.justifyContent.spaceBetween; - style.height(length.percent 100); - style.minHeight(35) - style.width(length.percent 100) - style.alignItems.center - yield! specificStyle - ] + let cellStyle (specificStyle: IStyleAttribute list) = prop.style [ + style.minWidth 100 + style.height 22 + style.border(length.px 1, borderStyle.solid, "darkgrey") + yield! specificStyle + ] -let private cellInputElement (isHeader: bool, updateMainStateTable: unit -> unit, setState_cell, state_cell, cell_value) = - Bulma.input.text [ - prop.autoFocus true - prop.style [ - if isHeader then style.fontWeight.bold + let cellInnerContainerStyle (specificStyle: IStyleAttribute list) = prop.style [ + style.display.flex; + style.justifyContent.spaceBetween; + style.height(length.percent 100); + style.minHeight(35) style.width(length.percent 100) - style.height.unset - style.borderRadius(0) - style.border(0,borderStyle.none,"") - style.backgroundColor.transparent - style.margin (0) - style.padding(length.em 0.5,length.em 0.75) - //if isHeader then - // style.color(NFDIColors.white) + style.alignItems.center + yield! specificStyle ] - // Update main spreadsheet state when leaving focus or... - prop.onBlur(fun _ -> - updateMainStateTable() - ) - // .. when pressing "ENTER". "ESCAPE" will negate changes. - prop.onKeyDown(fun e -> - match e.which with - | 13. -> //enter - updateMainStateTable() - | 27. -> //escape - setState_cell {Active = false; Value = cell_value} - | _ -> () - ) - // Only change cell value while typing to increase performance. - prop.onChange(fun e -> - setState_cell {state_cell with Value = e} - ) - prop.defaultValue cell_value - ] -let private basicValueDisplayCell (v: string) = - Html.span [ - prop.style [ - style.flexGrow 1 - style.padding(length.em 0.5,length.em 0.75) + 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"] + ]] + ] ] - prop.text v - ] + + let extendHeaderButton (state_extend: Set, columnIndex, setState_extend) = + let isExtended = state_extend.Contains(columnIndex) + Bulma.icon [ + prop.style [ + style.height (length.perc 100) + style.minWidth 25 + style.cursor.pointer + ] + prop.onDoubleClick(fun e -> + e.stopPropagation() + e.preventDefault() + () + ) + prop.onClick(fun e -> + e.stopPropagation() + e.preventDefault() + let nextState = if isExtended then state_extend.Remove(columnIndex) else state_extend.Add(columnIndex) + setState_extend nextState + ) + prop.children [Html.i [prop.classes ["fa-sharp"; "fa-solid"; "fa-angles-up"; if isExtended then "fa-rotate-270" else "fa-rotate-90"]; prop.style [style.fontSize(length.em 1)]]] + ] + + +module private CellAux = + + let headerTANSetter (columnIndex: int, s: string, header: CompositeHeader, dispatch) = + match header.TryOA(), s with + | Some oa, "" -> oa.TermAccessionNumber <- None; Some oa + | Some oa, s1 -> oa.TermAccessionNumber <- Some s1; Some oa + | None, _ -> None + |> Option.map header.UpdateWithOA + |> Option.iter (fun nextHeader -> Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch) + + let oasetter (index, nextCell: CompositeCell, dispatch) = Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + + +open CellComponents +open CellAux module private EventPresets = open Shared - let onClickSelect (index: int*int, state_cell, selectedCells: Set, model:Messages.Model, dispatch)= + let onClickSelect (index: int*int, isIdle:bool, selectedCells: Set, model:Messages.Model, dispatch)= fun (e: Browser.Types.MouseEvent) -> // don't select cell if active(editable) - if not state_cell.Active then + if isIdle then let set = - match e.ctrlKey with - | true -> + match e.shiftKey, selectedCells.Count with + | true, 0 -> + selectedCells + | true, _ -> let createSetOfIndex (columnMin:int, columnMax, rowMin:int, rowMax: int) = [ for c in columnMin .. columnMax do @@ -110,7 +127,7 @@ module private EventPresets = let rowMin, rowMax = System.Math.Min(snd source, snd target), System.Math.Max(snd source, snd target) let set = createSetOfIndex (columnMin,columnMax,rowMin,rowMax) set - | false -> + | false, _ -> let next = if selectedCells = Set([index]) then Set.empty else Set([index]) next UpdateSelectedCells set |> SpreadsheetMsg |> dispatch @@ -123,270 +140,264 @@ module private EventPresets = else None TermSearch.UpdateParentTerm oa |> TermSearchMsg |> dispatch -///// Only apply this element to SwateCell if header has term. -//[] -//let TANCell(index: (int*int), model: Model, dispatch) = -// let columnIndex = fst index -// let rowIndex = snd index -// let state = model.SpreadsheetModel -// let cell = state.ActiveTable.[index] -// let isHeader = cell.isHeader -// let cellValue = -// if isHeader then -// let tan = cell.Header.Term |> Option.map (fun x -> x.TermAccession) |> Option.defaultValue "" -// tan -// elif cell.isUnit then -// cell.Unit.Unit.TermAccession -// elif cell.isTerm then -// cell.Term.Term.TermAccession -// else "" -// let state_cell, setState_cell = React.useState(CellState.init(cellValue)) -// let isSelected = state.SelectedCells.Contains index -// let cell_element : IReactProperty list -> ReactElement = if isHeader then Html.th else Html.td -// cell_element [ -// prop.key $"Cell_{state.ActiveTableIndex}-{columnIndex}-{rowIndex}_TAN" -// cellStyle [ -// if isHeader then -// style.color(NFDIColors.white) -// style.backgroundColor(NFDIColors.DarkBlue.Lighter20) -// if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) -// ] -// prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) -// prop.children [ -// Html.div [ -// cellInnerContainerStyle [] -// prop.onDoubleClick(fun e -> -// e.preventDefault() -// e.stopPropagation() -// UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch -// if not state_cell.Active then setState_cell {state_cell with Active = true} -// ) -// prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, dispatch) -// prop.children [ -// if state_cell.Active then -// let updateMainStateTable() = -// // Only update if changed -// if state_cell.Value <> cellValue then -// // Updating unit name should remove unit tsr/tan -// let nextTerm = -// match cell with -// | IsHeader header -> -// let nextTerm = header.Term |> Option.map (fun t -> {t with TermAccession = state_cell.Value}) |> Option.defaultValue (TermMinimal.create "" state_cell.Value ) -// let nextHeader = {header with Term = Some nextTerm} -// IsHeader nextHeader -// | IsTerm t_cell -> -// let nextTermCell = -// let term = { t_cell.Term with TermAccession = state_cell.Value } -// { t_cell with Term = term } -// IsTerm nextTermCell -// | IsUnit u_cell -> -// let nextUnitCell = -// let unit = { u_cell.Unit with TermAccession = state_cell.Value } -// { u_cell with Unit = unit } -// IsUnit nextUnitCell -// | IsFreetext _ -> -// let t_cell = cell.toTermCell().Term -// let term = {t_cell.Term with TermAccession = state_cell.Value} -// let nextCell = {t_cell with Term = term} -// IsTerm nextCell -// Msg.UpdateTable (index, nextTerm) |> SpreadsheetMsg |> dispatch -// setState_cell {state_cell with Active = false} -// cellInputElement(isHeader, updateMainStateTable, setState_cell, state_cell, cellValue) -// else -// let displayValue = -// if isHeader then -// $"{ColumnCoreNames.TermAccessionNumber.toString} ({cellValue})" -// else -// cellValue -// Html.span [ -// prop.style [ -// style.flexGrow 1 -// ] -// prop.text displayValue -// ] -// ] -// ] -// ] -// ] -//[] -//let UnitCell(index: (int*int), model: Model, dispatch) = -// let columnIndex = fst index -// let rowIndex = snd index -// let state = model.SpreadsheetModel -// let cell = state.ActiveTable.[index] -// let isHeader = cell.isHeader -// let cellValue = if isHeader then ColumnCoreNames.Unit.toString elif cell.isUnit then cell.Unit.Unit.Name else "Unknown" -// let state_cell, setState_cell = React.useState(CellState.init(cellValue)) -// let isSelected = state.SelectedCells.Contains index -// let cell_element : IReactProperty list -> ReactElement = if isHeader then Html.th else Html.td -// cell_element [ -// prop.key $"Cell_{state.ActiveTableIndex}-{columnIndex}-{rowIndex}_Unit" -// cellStyle [ -// if isHeader then -// style.color(NFDIColors.white) -// style.backgroundColor(NFDIColors.DarkBlue.Lighter20) -// if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) -// ] -// prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) -// prop.children [ -// Html.div [ -// cellInnerContainerStyle [] -// prop.onDoubleClick(fun e -> -// e.preventDefault() -// e.stopPropagation() -// UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch -// if not state_cell.Active then setState_cell {state_cell with Active = true} -// ) -// prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, dispatch) -// prop.children [ -// if not isHeader && state_cell.Active then -// let updateMainStateTable() = -// // Only update if changed -// if state_cell.Value <> cellValue then -// // This column only exists for unit cells -// let nextTerm = {cell.Unit.Unit with Name = state_cell.Value} -// let nextBody = IsUnit { cell.Unit with Unit = nextTerm } -// Msg.UpdateTable (index, nextBody) |> SpreadsheetMsg |> dispatch -// setState_cell {state_cell with Active = false} -// cellInputElement(isHeader, updateMainStateTable, setState_cell, state_cell, cellValue) -// else -// Html.span [ -// prop.style [ -// style.flexGrow 1 -// ] -// prop.text cellValue -// ] -// ] -// ] -// ] -// ] +open Shared +open Fable.Core.JsInterop -//let private extendHeaderButton (state_extend: Set, columnIndex, setState_extend) = -// let isExtended = state_extend.Contains(columnIndex) -// Bulma.icon [ -// prop.style [ -// style.cursor.pointer -// ] -// prop.onDoubleClick(fun e -> -// e.stopPropagation() -// e.preventDefault() -// () -// ) -// prop.onClick(fun e -> -// e.stopPropagation() -// e.preventDefault() -// let nextState = if isExtended then state_extend.Remove(columnIndex) else state_extend.Add(columnIndex) -// setState_extend nextState -// ) -// prop.children [Html.i [prop.classes ["fa-sharp"; "fa-solid"; "fa-angles-up"; if isExtended then "fa-rotate-270" else "fa-rotate-90"]; prop.style [style.fontSize(length.em 1)]]] -// ] +type Cell = -[] -let HeaderCell(columnIndex: int, state_extend: Set, setState_extend, model: Model, dispatch) = - let state = model.SpreadsheetModel - let header = state.ActiveTable.Headers.[columnIndex] - let cellValue = header.ToString() - let state_cell, setState_cell = React.useState(CellState.init(cellValue)) - Html.th [ - prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}" - cellStyle [ - //if isHeader && cell.Header.isInputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.LightBlue.Base) - //elif isHeader && cell.Header.isOutputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.Red.Lighter30) - //elif isHeader then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.DarkBlue.Base) - //if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) - ] - //prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) - prop.children [ - Html.div [ - cellInnerContainerStyle [] - prop.onDoubleClick(fun e -> - e.preventDefault() - e.stopPropagation() - UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch - if not state_cell.Active then setState_cell {state_cell with Active = true} - ) - //prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, dispatch) + [] + static member CellInputElement (input: string, isHeader: bool, isReadOnly: bool, setter: string -> unit, makeIdle) = + let state, setState = React.useState(input) + React.useEffect((fun () -> setState input), [|box input|]) + let debounceStorage = React.useRef(newDebounceStorage()) + let loading, setLoading = React.useState(false) + let dsetter (inp) = debouncel debounceStorage.current "TextChange" 1000 setLoading setter inp + let input = + Bulma.control.div [ + Bulma.control.isExpanded + if loading then Bulma.control.isLoading prop.children [ - if state_cell.Active then - /// Update change to mainState and exit active input. - let updateMainStateTable() = - // Only update if changed - if state_cell.Value <> cellValue then - let nextHeader = CompositeHeader.OfHeaderString state_cell.Value - Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch - setState_cell {state_cell with Active = false} - cellInputElement(true, updateMainStateTable, setState_cell, state_cell, cellValue) - else - basicValueDisplayCell cellValue - //if isHeader && cell.Header.isTermColumn then - // extendHeaderButton(state_extend, columnIndex, setState_extend) + Bulma.input.text [ + prop.defaultValue input + prop.readOnly isReadOnly + prop.autoFocus true + prop.style [ + if isHeader then style.fontWeight.bold + style.width(length.percent 100) + style.height.unset + style.borderRadius(0) + style.border(0,borderStyle.none,"") + style.backgroundColor.transparent + style.margin (0) + style.padding(length.em 0.5,length.em 0.75) + ] + prop.onBlur(fun _ -> + if isHeader then setter state; + makeIdle() + ) + prop.onKeyDown(fun e -> + e.stopPropagation() + match e.which with + | 13. -> //enter + if isHeader then setter state + makeIdle() + | 27. -> //escape + makeIdle() + | _ -> () + ) + // Only change cell value while typing to increase performance. + prop.onChange(fun e -> + if isHeader then setState e else dsetter e + ) + ] ] ] + Bulma.field.div [ + Bulma.field.hasAddons + prop.className "is-flex-grow-1 m-0" + prop.children [ input ] ] - ] -[] -let BodyCell(index: (int*int), state_extend: Set, setState_extend, model: Model, dispatch) = - let columnIndex, rowIndex = index - let state = model.SpreadsheetModel - let cell = state.ActiveTable.TryGetCellAt index |> Option.defaultValue CompositeCell.emptyFreeText - let cellValue = cell.GetContent().[0] - let state_cell, setState_cell = React.useState(CellState.init(cellValue)) - let isSelected = state.SelectedCells.Contains index - Html.td [ - prop.key $"Cell_{state.ActiveView.TableIndex}-{columnIndex}-{rowIndex}" - cellStyle [ - //if isHeader && cell.Header.isInputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.LightBlue.Base) - //elif isHeader && cell.Header.isOutputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.Red.Lighter30) - //elif isHeader then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.DarkBlue.Base) - if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) + [] + static member private HeaderBase(columnType: ColumnType, setter: string -> unit, cellValue: string, columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let state = model.SpreadsheetModel + let isReadOnly = columnType = Unit + let makeIdle() = UpdateActiveCell None |> SpreadsheetMsg |> dispatch + let makeActive() = UpdateActiveCell (Some (!^columnIndex, columnType)) |> SpreadsheetMsg |> dispatch + let isIdle = state.CellIsIdle (!^columnIndex, columnType) + let isActive = not isIdle + Html.th [ + if columnType.IsRefColumn then Bulma.color.hasBackgroundGreyLighter + prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}-{columnType}" + prop.id $"Header_{columnIndex}_{columnType}" + cellStyle [] + prop.className "main-contrast-bg" + prop.children [ + Html.div [ + cellInnerContainerStyle [style.custom("backgroundColor","inherit")] + if not isReadOnly then prop.onDoubleClick(fun e -> + e.preventDefault() + e.stopPropagation() + UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch + if isIdle then makeActive() + ) + prop.children [ + if isActive then + Cell.CellInputElement(cellValue, true, isReadOnly, setter, makeIdle) + else + let cellValue = // shadow cell value for tsr and tan to add columnType + match columnType with + | TSR | TAN -> $"{columnType} ({cellValue})" + | _ -> cellValue + basicValueDisplayCell cellValue + if columnType = Main && not header.IsSingleColumn then + extendHeaderButton(state_extend, columnIndex, setState_extend) + ] + ] + ] ] - prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) - prop.children [ + + static member Header(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let cellValue = header.ToString() + let setter = + fun (s: string) -> + let mutable nextHeader = CompositeHeader.OfHeaderString s + // update header with ref columns if term column + if header.IsTermColumn && not header.IsFeaturedColumn then + let updatedOA = + match nextHeader.TryOA() ,header.TryOA() with + | Some oa1, Some oa2 -> oa1.TermAccessionNumber <- oa2.TermAccessionNumber; oa1.TermSourceREF <- oa2.TermSourceREF; oa1 + | _ -> 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) + + static member HeaderUnit(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let cellValue = "Unit" + let setter = fun (s: string) -> () + Cell.HeaderBase(Unit, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + + static member HeaderTSR(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let cellValue = header.TryOA() |> Option.map (fun oa -> oa.TermAccessionShort) |> Option.defaultValue "" + let setter = fun (s: string) -> headerTANSetter(columnIndex, s, header, dispatch) + Cell.HeaderBase(TSR, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + + static member HeaderTAN(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let cellValue = header.TryOA() |> Option.map (fun oa -> oa.TermAccessionShort) |> Option.defaultValue "" + let setter = fun (s: string) -> headerTANSetter(columnIndex, s, header, dispatch) + Cell.HeaderBase(TAN, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + + static member Empty() = + Html.td [ cellStyle []; prop.readOnly true; prop.children [ Html.div [ - cellInnerContainerStyle [] - prop.onDoubleClick(fun e -> - e.preventDefault() - e.stopPropagation() - UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch - if not state_cell.Active then setState_cell {state_cell with Active = true} - ) - prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, model, dispatch) + prop.style [style.height (length.perc 100); style.cursor.notAllowed; style.userSelect.none] + prop.className "is-flex is-align-items-center is-justify-content-center has-background-grey-lighter" prop.children [ - if state_cell.Active then - /// Update change to mainState and exit active input. - let updateMainStateTable() = - // Only update if changed - if state_cell.Value <> cellValue then - let nextCell = cell.UpdateMainField state_cell.Value - Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - setState_cell {state_cell with Active = false} - cellInputElement(false, updateMainStateTable, setState_cell, state_cell, cellValue) - else - let displayName = - if cell.isUnitized then - let oaName = cell.ToOA().NameText - let unitizedValue = if cellValue = "" then "" else cellValue + " " + oaName - unitizedValue + Html.div "-" + ] + ] + ]] + + [] + static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch, ?oasetter: OntologyAnnotation -> unit) = + let columnIndex, rowIndex = index + let state = model.SpreadsheetModel + let isSelected = state.SelectedCells.Contains index + let isIdle = state.CellIsIdle (!^index, columnType) + let isActive = not isIdle + let ref = React.useElementRef() + let makeIdle() = + UpdateActiveCell None |> SpreadsheetMsg |> dispatch + let ele = Browser.Dom.document.getElementById("SPREADSHEET_MAIN_VIEW") + ele.focus() + let makeActive() = UpdateActiveCell (Some (!^index, columnType)) |> SpreadsheetMsg |> dispatch + React.useEffect((fun () -> + if isSelected then + let options = createEmpty + options.behavior <- Browser.Types.ScrollBehavior.Auto + options.``inline`` <- Browser.Types.ScrollAlignment.Nearest + options.block <- Browser.Types.ScrollAlignment.Nearest + if ref.current.IsSome then ref.current.Value.scrollIntoView(options)), + [|box isSelected|] + ) + Html.td [ + prop.key $"Cell_{state.ActiveView.TableIndex}-{columnIndex}-{rowIndex}" + cellStyle [ + if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) + ] + prop.ref ref + prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) + 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) + 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 "" + 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 else - cellValue - basicValueDisplayCell displayName - //if isHeader && cell.Header.isTermColumn then - // extendHeaderButton(state_extend, columnIndex, setState_extend) + basicValueDisplayCell cellValue + ] ] ] ] - ] \ No newline at end of file + + static member Body(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + let cellValue = cell.GetContent().[0] + let setter = fun (s: string) -> + let nextCell = cell.UpdateMainField s + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + let oasetter = + if cell.isTerm then + fun (oa:OntologyAnnotation) -> + let nextCell = cell.UpdateWithOA oa + CellAux.oasetter(index, nextCell, dispatch) + |> Some + else + None + Cell.BodyBase(Main, cellValue, setter, index, cell, model, dispatch, ?oasetter=oasetter) + + static member BodyUnit(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + let cellValue = cell.GetContent().[1] + let setter = fun (s: string) -> + let oa = cell.ToOA() + let newName = if s = "" then None else Some s + oa.Name <- newName + let nextCell = cell.UpdateWithOA oa + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + let oasetter = + if cell.isUnitized then + log "IsUnitized" + fun (oa:OntologyAnnotation) -> + log ("oa", oa) + let nextCell = cell.UpdateWithOA oa + log ("nextCell", nextCell) + CellAux.oasetter(index, nextCell, dispatch) + |> Some + else + None + Cell.BodyBase(Unit, cellValue, setter, index, cell, model, dispatch, ?oasetter=oasetter) + + static member BodyTSR(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + let contentIndex = if cell.isUnitized then 2 else 1 + let cellValue = cell.GetContent().[contentIndex] + let setter = fun (s: string) -> + let oa = cell.ToOA() + let newTSR = if s = "" then None else Some s + oa.TermSourceREF <- newTSR + let nextCell = cell.UpdateWithOA oa + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + Cell.BodyBase(TSR, cellValue, setter, index, cell, model, dispatch) + + static member BodyTAN(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + let contentIndex = if cell.isUnitized then 3 else 2 + let cellValue = cell.GetContent().[contentIndex] + let setter = fun (s: string) -> + let oa = cell.ToOA() + let newTAN = if s = "" then None else Some s + oa.TermAccessionNumber <- newTAN + let nextCell = cell.UpdateWithOA oa + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + Cell.BodyBase(TAN, cellValue, setter, index, cell, model, dispatch) + \ No newline at end of file diff --git a/src/Client/MainComponents/ContextMenu.fs b/src/Client/MainComponents/ContextMenu.fs index c89af432..0da1034d 100644 --- a/src/Client/MainComponents/ContextMenu.fs +++ b/src/Client/MainComponents/ContextMenu.fs @@ -3,22 +3,30 @@ module MainComponents.ContextMenu open Feliz open Feliz.Bulma open Spreadsheet -open ARCtrl.ISA +open ARCtrl open Messages 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 contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (selectedCell: CompositeCell option ) (rmv: _ -> unit) = +let private isUnitOrTermCell (cell: CompositeCell option) = + cell.IsSome && not cell.Value.isFreeText + +let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (contextCell: CompositeCell option) (rmv: _ -> unit) = /// This element will remove the contextmenu when clicking anywhere else let rmv_element = Html.div [ prop.onClick rmv @@ -35,8 +43,9 @@ let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (sel ] let button (name:string, icon: string, msg, props) = Html.li [ Bulma.button.button [ - prop.style [style.borderRadius 0; style.justifyContent.spaceBetween] + 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 @@ -49,18 +58,26 @@ let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (sel ] ] let divider = Html.li [ - Html.div [ prop.style [style.border(2, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0)] ] + 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-file-signature", funcs.FillColumn 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, [prop.disabled selectedCell.IsNone]) + button ("Paste", "fa-solid fa-paste", funcs.Paste rmv, []) + button ("Paste All", "fa-solid fa-paste", funcs.PasteAll rmv, []) divider button ("Delete Row", "fa-solid fa-delete-left", funcs.DeleteRow rmv, []) button ("Delete Column", "fa-solid fa-delete-left fa-rotate-270", funcs.DeleteColumn rmv, []) + button ("Move Column", "fa-solid fa-arrow-right-arrow-left", funcs.MoveColumn rmv, []) ] Html.div [ prop.style [ @@ -78,6 +95,8 @@ let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (sel ] ] +open Shared + let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Types.MouseEvent) -> e.stopPropagation() e.preventDefault() @@ -90,18 +109,45 @@ let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Typ Spreadsheet.DeleteRows indexArr |> Messages.SpreadsheetMsg |> dispatch else Spreadsheet.DeleteRow (snd index) |> Messages.SpreadsheetMsg |> dispatch + let cell = model.SpreadsheetModel.ActiveTable.TryGetCellAt(fst index, snd index) + let 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 - Copy = fun rmv e -> rmv e; Spreadsheet.CopyCell 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; Spreadsheet.PasteCell 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 model.SpreadsheetModel.Clipboard.Cell + let child = contextmenu mousePosition funcs cell let name = $"context_{mousePosition}" Modals.Controller.renderModal(name, child) \ No newline at end of file diff --git a/src/Client/MainComponents/EmptyTableElement.fs b/src/Client/MainComponents/EmptyTableElement.fs new file mode 100644 index 00000000..56b74273 --- /dev/null +++ b/src/Client/MainComponents/EmptyTableElement.fs @@ -0,0 +1,54 @@ +namespace MainComponents + +open Feliz +open Feliz.Bulma +open ARCtrl + +type EmptyTableElement = + static member Main(openBuildingBlockWidget: unit -> unit, openTemplateWidget: unit -> unit) = + Html.div [ + prop.className "is-flex is-justify-content-center is-align-items-center" + prop.style [style.height (length.perc 100)] + prop.children [ + Bulma.box [ + Bulma.content [ + prop.children [ + Html.h3 [ + prop.className "title" + prop.text "New Table!" + ] + Bulma.field.div [ + prop.className "is-flex is-justify-content-space-between is-align-items-center gap-3" + prop.children [ + Html.text "Start from an existing template!" + Bulma.button.span [ + prop.onClick (fun _ -> openTemplateWidget()) + prop.children [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table" ] + ] + ] + ] + ] + ] + Bulma.field.div [ + prop.className "is-flex is-justify-content-space-between is-align-items-center gap-3" + prop.children [ + Html.text "Or start from scratch!" + Bulma.button.span [ + prop.onClick (fun _ -> openBuildingBlockWidget()) + prop.children [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table-columns" ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] \ No newline at end of file diff --git a/src/Client/MainComponents/FooterTabs.fs b/src/Client/MainComponents/FooterTabs.fs index a8bb4cc5..60a96953 100644 --- a/src/Client/MainComponents/FooterTabs.fs +++ b/src/Client/MainComponents/FooterTabs.fs @@ -2,7 +2,7 @@ module MainComponents.FooterTabs open Feliz open Feliz.Bulma -open ARCtrl.ISA +open ARCtrl type private FooterTab = { IsEditable: bool @@ -95,11 +95,9 @@ let private dragleave_handler(state, setState) = fun (e: Browser.Types.DragEvent setState {state with IsDraggedOver = false} [] -let Main (input: {|index: int; tables: ArcTables; model: Messages.Model; dispatch: Messages.Msg -> unit|}) = - let index = input.index - let table = input.tables.GetTableAt(index) +let Main (index: int, tables: ArcTables, model: Messages.Model, dispatch: Messages.Msg -> unit) = + let table = tables.GetTableAt(index) let state, setState = React.useState(FooterTab.init(table.Name)) - let dispatch = input.dispatch let id = $"ReorderMe_{index}_{table.Name}" Bulma.tab [ if state.IsDraggedOver then prop.className "dragover-footertab" @@ -120,7 +118,7 @@ let Main (input: {|index: int; tables: ArcTables; model: Messages.Model; dispatc // Use this to ensure updating reactelement correctly prop.key id prop.id id - if input.model.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Table index then Bulma.tab.isActive + if model.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Table index then Bulma.tab.isActive prop.onClick (fun _ -> Spreadsheet.UpdateActiveView (Spreadsheet.ActiveView.Table index) |> Messages.SpreadsheetMsg |> dispatch) prop.onContextMenu(fun e -> e.stopPropagation() @@ -136,7 +134,8 @@ let Main (input: {|index: int; tables: ArcTables; model: Messages.Model; dispatc prop.children [ if state.IsEditable then let updateName = fun e -> - Spreadsheet.RenameTable (index, state.Name) |> Messages.SpreadsheetMsg |> dispatch + if state.Name <> table.Name then + Spreadsheet.RenameTable (index, state.Name) |> Messages.SpreadsheetMsg |> dispatch setState {state with IsEditable = false} Bulma.input.text [ prop.autoFocus(true) @@ -162,13 +161,12 @@ let Main (input: {|index: int; tables: ArcTables; model: Messages.Model; dispatc ] [] -let MainMetadata(input:{|model: Messages.Model; dispatch: Messages.Msg -> unit|}) = - let dispatch = input.dispatch +let MainMetadata(model: Messages.Model, dispatch: Messages.Msg -> unit) = let order = 0 let id = "Metadata-Tab" let nav = Spreadsheet.ActiveView.Metadata Bulma.tab [ - if input.model.SpreadsheetModel.ActiveView = nav then Bulma.tab.isActive + if model.SpreadsheetModel.ActiveView = nav then Bulma.tab.isActive prop.key id prop.id id prop.onClick (fun _ -> Spreadsheet.UpdateActiveView nav |> Messages.SpreadsheetMsg |> dispatch) @@ -179,10 +177,9 @@ let MainMetadata(input:{|model: Messages.Model; dispatch: Messages.Msg -> unit|} ] [] -let MainPlus(input:{|dispatch: Messages.Msg -> unit|}) = - let dispatch = input.dispatch +let MainPlus(model: Messages.Model, dispatch: Messages.Msg -> unit) = let state, setState = React.useState(FooterTab.init()) - let order = System.Int32.MaxValue + let order = System.Int32.MaxValue-1 // MaxValue will be sidebar toggle let id = "Add-Spreadsheet-Button" Bulma.tab [ prop.key id @@ -207,4 +204,28 @@ let MainPlus(input:{|dispatch: Messages.Msg -> unit|}) = ] ] ] + ] + +let ToggleSidebar(model: Messages.Model, dispatch: Messages.Msg -> unit) = + let show = model.PersistentStorageState.ShowSideBar + let order = System.Int32.MaxValue + let id = "Toggle-Sidebar-Button" + Bulma.tab [ + prop.key id + prop.id id + prop.onClick (fun e -> Messages.PersistentStorage.UpdateShowSidebar (not show) |> Messages.PersistentStorageMsg |> dispatch) + prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer; style.marginLeft length.auto] + prop.children [ + Html.a [ + prop.style [style.height.inheritFromParent; style.pointerEvents.none] + prop.children [ + Bulma.icon [ + Bulma.icon.isSmall + prop.children [ + Html.i [prop.className ["fa-solid"; if show then "fa-chevron-right" else "fa-chevron-left"]] + ] + ] + ] + ] + ] ] \ No newline at end of file diff --git a/src/Client/MainComponents/KeyboardShortcuts.fs b/src/Client/MainComponents/KeyboardShortcuts.fs index 3ffeaf41..8f51397f 100644 --- a/src/Client/MainComponents/KeyboardShortcuts.fs +++ b/src/Client/MainComponents/KeyboardShortcuts.fs @@ -1,26 +1,31 @@ module Spreadsheet.KeyboardShortcuts -let private onKeydownEvent (dispatch: Messages.Msg -> unit) = +let onKeydownEvent (dispatch: Messages.Msg -> unit) = fun (e: Browser.Types.Event) -> - //e.preventDefault() - //e.stopPropagation() let e = e :?> Browser.Types.KeyboardEvent - match e.ctrlKey, e.which with - | false, _ -> () + match (e.ctrlKey || e.metaKey), e.which with + | false, 27. | false, 13. | false, 9. | false, 16. -> // escape, enter, tab, shift + () + | false, 46. -> // del + Spreadsheet.ClearSelected |> Messages.SpreadsheetMsg |> dispatch + | false, 37. -> // arrow left + MoveSelectedCell Key.Left |> Messages.SpreadsheetMsg |> dispatch + | false, 38. -> // arrow up + MoveSelectedCell Key.Up |> Messages.SpreadsheetMsg |> dispatch + | false, 39. -> // arrow right + MoveSelectedCell Key.Right |> Messages.SpreadsheetMsg |> dispatch + | false, 40. -> // arrow down + MoveSelectedCell Key.Down |> Messages.SpreadsheetMsg |> dispatch + | false, key -> + SetActiveCellFromSelected |> Messages.SpreadsheetMsg |> dispatch // Ctrl + c - | _, _ -> - match e.ctrlKey, e.which with - | true, 67. -> - Spreadsheet.CopySelectedCell |> Messages.SpreadsheetMsg |> dispatch - // Ctrl + c - | true, 88. -> - Spreadsheet.CutSelectedCell |> Messages.SpreadsheetMsg |> dispatch - // Ctrl + v - | true, 86. -> - Spreadsheet.PasteSelectedCell |> Messages.SpreadsheetMsg |> dispatch - | _, _ -> () + | true, _ -> + match e.which with + | 67. -> // Ctrl + c + Spreadsheet.CopySelectedCells |> Messages.SpreadsheetMsg |> dispatch + | 88. -> // Ctrl + x + Spreadsheet.CutSelectedCells |> Messages.SpreadsheetMsg |> dispatch + | 86. -> // Ctrl + v + Spreadsheet.PasteSelectedCells |> Messages.SpreadsheetMsg |> dispatch + | _ -> () -///These events only get reapplied on reload, not during hot reload -let addOnKeydownEvent dispatch = - Browser.Dom.document.body.removeEventListener("keydown", onKeydownEvent dispatch) - Browser.Dom.document.body.addEventListener("keydown", onKeydownEvent dispatch) diff --git a/src/Client/MainComponents/MainViewContainer.fs b/src/Client/MainComponents/MainViewContainer.fs new file mode 100644 index 00000000..48f29d9f --- /dev/null +++ b/src/Client/MainComponents/MainViewContainer.fs @@ -0,0 +1,19 @@ +module MainComponents.MainViewContainer + + +open Feliz +open Feliz.Bulma +open Spreadsheet +open Messages + +let Main(minWidth: int, left: ReactElement seq) = + Html.div [ + prop.style [ + style.minWidth(minWidth) + style.flexGrow 1 + style.flexShrink 1 + style.height(length.vh 100) + style.width(length.perc 100) + ] + prop.children left + ] \ No newline at end of file diff --git a/src/Client/MainComponents/Metadata/Assay.fs b/src/Client/MainComponents/Metadata/Assay.fs index 99878715..15064d6d 100644 --- a/src/Client/MainComponents/Metadata/Assay.fs +++ b/src/Client/MainComponents/Metadata/Assay.fs @@ -3,9 +3,10 @@ open Feliz open Feliz.Bulma open Messages -open ARCtrl.ISA +open ARCtrl open Shared +[] let Main(assay: ArcAssay, model: Messages.Model, dispatch: Msg -> unit) = Bulma.section [ FormComponents.TextInput ( @@ -17,41 +18,41 @@ let Main(assay: ArcAssay, model: Messages.Model, dispatch: Msg -> unit) = fullwidth=true ) FormComponents.OntologyAnnotationInput( - assay.MeasurementType |> Option.defaultValue OntologyAnnotation.empty, - "Measurement Type", - fun oa -> - let oa = if oa = OntologyAnnotation.empty then None else Some oa + assay.MeasurementType |> Option.defaultValue (OntologyAnnotation.empty()), + (fun oa -> + let oa = if oa = (OntologyAnnotation.empty()) then None else Some oa assay.MeasurementType <- oa - assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), + "Measurement Type" ) FormComponents.OntologyAnnotationInput( - assay.TechnologyType |> Option.defaultValue OntologyAnnotation.empty, - "Technology Type", - fun oa -> - let oa = if oa = OntologyAnnotation.empty then None else Some oa + assay.TechnologyType |> Option.defaultValue (OntologyAnnotation.empty()), + (fun oa -> + let oa = if oa = (OntologyAnnotation.empty()) then None else Some oa assay.TechnologyType <- oa - assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), + "Technology Type" ) FormComponents.OntologyAnnotationInput( - assay.TechnologyPlatform |> Option.defaultValue OntologyAnnotation.empty, - "Technology Platform", - fun oa -> - let oa = if oa = OntologyAnnotation.empty then None else Some oa + assay.TechnologyPlatform |> Option.defaultValue (OntologyAnnotation.empty()), + (fun oa -> + let oa = if oa = (OntologyAnnotation.empty()) then None else Some oa assay.TechnologyPlatform <- oa - assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), + "Technology Platform" ) FormComponents.PersonsInput( - assay.Performers, + Array.ofSeq assay.Performers, "Performers", fun persons -> - assay.Performers <- persons + assay.Performers <- ResizeArray persons assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch ) FormComponents.CommentsInput( - assay.Comments, + Array.ofSeq assay.Comments, "Comments", fun comments -> - assay.Comments <- comments + assay.Comments <- ResizeArray comments assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch ) ] \ No newline at end of file diff --git a/src/Client/MainComponents/Metadata/Forms.fs b/src/Client/MainComponents/Metadata/Forms.fs index 5ee4244c..90b2ae5d 100644 --- a/src/Client/MainComponents/Metadata/Forms.fs +++ b/src/Client/MainComponents/Metadata/Forms.fs @@ -1,4 +1,4 @@ -namespace MainComponents.Metadata +namespace MainComponents.Metadata open Feliz open Feliz.Bulma @@ -7,139 +7,260 @@ open Spreadsheet open Messages open Browser.Types open Fable.Core.JsInterop -open ARCtrl.ISA +open ARCtrl open Shared +open Fetch +open ARCtrl.Json +module private API = -module Helper = - type PersonMutable(?firstname, ?lastname, ?midinitials, ?orcid, ?address, ?affiliation, ?email, ?phone, ?fax, ?roles) = - member val FirstName : string option = firstname with get, set - member val LastName : string option = lastname with get, set - member val MidInitials : string option = midinitials with get, set - member val ORCID : string option = orcid with get, set - member val Address : string option = address with get, set - member val Affiliation : string option = affiliation with get, set - member val EMail : string option = email with get, set - member val Phone : string option = phone with get, set - member val Fax : string option = fax with get, set - member val Roles : OntologyAnnotation [] option = roles with get, set + [] + type Request<'A> = + | Ok of 'A + | Error of exn + | Loading + | Idle + + module Null = + let defaultValue (def:'A) (x:'A) = if isNull x then def else x + + let requestAsJson (url) = + promise { + let! response = fetch url [ + requestHeaders [Accept "application/json"] + ] + let! json = response.json() + return Some json + } + + let private createAuthorString (authors: Person []) = + authors |> Array.map (fun x -> $"{x.FirstName} {x.LastName}") |> String.concat ", " + + let private createAffiliationString (org_department: (string*string) []) : string option = + if org_department.Length = 0 then + None + else + org_department |> Array.map (fun (org,department) -> $"{org}, {department}") |> String.concat ";" + |> Some + + let requestByORCID (orcid: string) = + let url = $"https://pub.orcid.org/v3.0/{orcid}/record" + promise { + let! json = requestAsJson url + let name: string = json?person?name?("given-names")?value + let lastName: string = json?person?name?("family-name")?value + let emails: obj [] = json?person?emails?email + let email = if emails.Length = 0 then None else Some (emails.[0]?email) + let groups: obj [] = json?("activities-summary")?employments?("affiliation-group") + let groupsParsed = + groups |> Array.choose (fun json -> + let summaries : obj [] = json?summaries + let summary = + summaries + |> Array.tryHead + |> Option.map (fun s0 -> + let s = s0?("employment-summary") + let department = s?("department-name") |> Null.defaultValue "" + let org = s?organization?name |> Null.defaultValue "" + org, department + ) + summary + ) + |> createAffiliationString + let person = Person.create(orcid=orcid,lastName=lastName, firstName=name, ?email=email, ?affiliation=groupsParsed) + return person + } + + + let requestByPubMedID (id: string) = + let url = @"https://api.ncbi.nlm.nih.gov/lit/ctxp/v1/pubmed/?format=csl&id=" + id + promise { + let! json = requestAsJson url + let doi: string = json?DOI + let pmid: string = json?PMID + let authors : Person [] = + [| + for pj in json?author do + Person.create(LastName=pj?family, FirstName=pj?given) + |] + let authorString = createAuthorString authors + let title = json?title + let publication = Publication.create(pmid, doi, authorString, title, TermCollection.Published) + return publication + } + + let requestByDOI_FromPubMed (doi: string) = + /// https://academia.stackexchange.com/questions/67103/is-there-any-api-service-to-retrieve-abstract-of-a-journal-article + let url_pubmed = $"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term={doi}&retmode=JSON" + promise { + let! json = requestAsJson url_pubmed + let errorList = json?esearchresult?errorlist + if isNull errorList then + let idList : string [] = json?esearchresult?idlist + let pubmedID = if idList.Length <> 1 then None else Some idList.[0] + return pubmedID + else + return None + } + + let requestByDOI (doi: string) = + let url_crossref = $"https://api.crossref.org/works/{doi}" + promise { + let! json = requestAsJson url_crossref + let titles: string [] = json?message?title + let title = if titles.Length = 0 then None else Some titles.[0] + let authors : Person [] = [| + for pj in json?message?author do + let affiliationsJson: obj [] = pj?affiliation + let affiliations : string [] = + [| + for aff in affiliationsJson do + yield aff?("name") + |] + let affString = affiliations |> String.concat ", " + Person.create(ORCID=pj?ORCID, LastName=pj?family, FirstName=pj?given, affiliation=affString) + |] + let! pubmedId = requestByDOI_FromPubMed doi + let authorString = createAuthorString authors + let publication = Publication.create(?pubMedID=pubmedId, doi=doi, authors=authorString, ?title=title, status=TermCollection.Published) + return publication + } + + let start (call: 't -> Fable.Core.JS.Promise<'a>) (args:'t) (success) (fail) = + call args + |> Promise.either + success + fail + |> Promise.start + +module private Helper = + //type PersonMutable(?firstname, ?lastname, ?midinitials, ?orcid, ?address, ?affiliation, ?email, ?phone, ?fax, ?roles) = + // member val FirstName : string option = firstname with get, set + // member val LastName : string option = lastname with get, set + // member val MidInitials : string option = midinitials with get, set + // member val ORCID : string option = orcid with get, set + // member val Address : string option = address with get, set + // member val Affiliation : string option = affiliation with get, set + // member val EMail : string option = email with get, set + // member val Phone : string option = phone with get, set + // member val Fax : string option = fax with get, set + // member val Roles : OntologyAnnotation [] option = roles with get, set - static member fromPerson(person:Person) = - PersonMutable( - ?firstname=person.FirstName, - ?lastname=person.LastName, - ?midinitials=person.MidInitials, - ?orcid=person.ORCID, - ?address=person.Address, - ?affiliation=person.Affiliation, - ?email=person.EMail, - ?phone=person.Phone, - ?fax=person.Fax, - ?roles=person.Roles - ) + // static member fromPerson(person:Person) = + // PersonMutable( + // ?firstname=person.FirstName, + // ?lastname=person.LastName, + // ?midinitials=person.MidInitials, + // ?orcid=person.ORCID, + // ?address=person.Address, + // ?affiliation=person.Affiliation, + // ?email=person.EMail, + // ?phone=person.Phone, + // ?fax=person.Fax, + // ?roles=person.Roles + // ) - member this.ToPerson() = - Person.create( - ?FirstName=this.FirstName, - ?LastName=this.LastName, - ?MidInitials=this.MidInitials, - ?ORCID=this.ORCID, - ?Address=this.Address, - ?Affiliation=this.Affiliation, - ?Email=this.EMail, - ?Phone=this.Phone, - ?Fax=this.Fax, - ?Roles=this.Roles - ) + // member this.ToPerson() = + // Person.create( + // ?FirstName=this.FirstName, + // ?LastName=this.LastName, + // ?MidInitials=this.MidInitials, + // ?ORCID=this.ORCID, + // ?Address=this.Address, + // ?Affiliation=this.Affiliation, + // ?Email=this.EMail, + // ?Phone=this.Phone, + // ?Fax=this.Fax, + // ?Roles=this.Roles + // ) - type OntologyAnnotationMutable(?name,?tsr,?tan) = - member val Name : string option = name with get, set - member val TSR : string option = tsr with get, set - member val TAN : string option = tan with get, set - - static member fromOntologyAnnotation(oa: OntologyAnnotation) = - let name = if oa.NameText = "" then None else Some oa.NameText - OntologyAnnotationMutable(?name=name, ?tsr=oa.TermSourceREF, ?tan=oa.TermAccessionNumber) - - member this.ToOntologyAnnotation() = - OntologyAnnotation.fromString(?termName=this.Name,?tsr=this.TSR,?tan=this.TAN) - - type PublicationMutable(?pubmedid: string, ?doi: string, ?authors: string, ?title: string, ?status: OntologyAnnotation, ?comments: Comment []) = - member val PubmedId = pubmedid with get, set - member val Doi = doi with get, set - member val Authors = authors with get, set - member val Title = title with get, set - member val Status = status with get, set - member val Comments = comments with get, set - - static member fromPublication(pub:Publication) = - PublicationMutable( - ?pubmedid=pub.PubMedID, - ?doi=pub.DOI, - ?authors=pub.Authors, - ?title=pub.Title, - ?status=pub.Status, - ?comments=pub.Comments - ) + //type OntologyAnnotationMutable(?name,?tsr,?tan) = + // member val Name : string option = name with get, set + // member val TSR : string option = tsr with get, set + // member val TAN : string option = tan with get, set - member this.ToPublication() = - Publication.create( - ?PubMedID=this.PubmedId, - ?Doi=this.Doi, - ?Authors=this.Authors, - ?Title=this.Title, - ?Status=this.Status, - ?Comments=this.Comments - ) + // static member fromOntologyAnnotation(oa: OntologyAnnotation) = + // let name = if oa.NameText = "" then None else Some oa.NameText + // OntologyAnnotationMutable(?name=name, ?tsr=oa.TermSourceREF, ?tan=oa.TermAccessionNumber) - type FactorMutable(?name,?factortype,?comments) = - member val Name = name with get, set - member val FactorType = factortype with get, set - member val Comments = comments with get, set + // member this.ToOntologyAnnotation() = + // OntologyAnnotation.fromString(?termName=this.Name,?tsr=this.TSR,?tan=this.TAN) - static member fromFactor(f:Factor) = - FactorMutable( - ?name=f.Name, - ?factortype=f.FactorType, - ?comments=f.Comments - ) - member this.ToFactor() = - Factor.create( - ?Name=this.Name, - ?FactorType=this.FactorType, - ?Comments=this.Comments - ) + //type PublicationMutable(?pubmedid: string, ?doi: string, ?authors: string, ?title: string, ?status: OntologyAnnotation, ?comments: Comment []) = + // member val PubmedId = pubmedid with get, set + // member val Doi = doi with get, set + // member val Authors = authors with get, set + // member val Title = title with get, set + // member val Status = status with get, set + // member val Comments = comments with get, set - type OntologySourceReferenceMutable(?name,?description,?file,?version,?comments) = - member val Name = name with get, set - member val Description = description with get, set - member val File = file with get, set - member val Version = version with get, set - member val Comments = comments with get, set - - static member fromOntologySourceReference(o:OntologySourceReference) = - OntologySourceReferenceMutable( - ?name=o.Name, - ?description= o.Description, - ?file=o.File, - ?version=o.Version, - ?comments=o.Comments - ) - member this.ToOntologySourceReference() = - OntologySourceReference.create( - ?Name=this.Name, - ?Description=this.Description, - ?File=this.File, - ?Version=this.Version, - ?Comments=this.Comments - ) + // static member fromPublication(pub:Publication) = + // PublicationMutable( + // ?pubmedid=pub.PubMedID, + // ?doi=pub.DOI, + // ?authors=pub.Authors, + // ?title=pub.Title, + // ?status=pub.Status, + // ?comments=pub.Comments + // ) + + // member this.ToPublication() = + // Publication.create( + // ?PubMedID=this.PubmedId, + // ?Doi=this.Doi, + // ?Authors=this.Authors, + // ?Title=this.Title, + // ?Status=this.Status, + // ?Comments=this.Comments + // ) + + //type FactorMutable(?name,?factortype,?comments) = + // member val Name = name with get, set + // member val FactorType = factortype with get, set + // member val Comments = comments with get, set + + // static member fromFactor(f:Factor) = + // FactorMutable( + // ?name=f.Name, + // ?factortype=f.FactorType, + // ?comments=f.Comments + // ) + // member this.ToFactor() = + // Factor.create( + // ?Name=this.Name, + // ?FactorType=this.FactorType, + // ?Comments=this.Comments + // ) + + //type OntologySourceReferenceMutable(?name,?description,?file,?version,?comments) = + // member val Name = name with get, set + // member val Description = description with get, set + // member val File = file with get, set + // member val Version = version with get, set + // member val Comments = comments with get, set + + // static member fromOntologySourceReference(o:OntologySourceReference) = + // OntologySourceReferenceMutable( + // ?name=o.Name, + // ?description= o.Description, + // ?file=o.File, + // ?version=o.Version, + // ?comments=o.Comments + // ) + // member this.ToOntologySourceReference() = + // OntologySourceReference.create( + // ?Name=this.Name, + // ?Description=this.Description, + // ?File=this.File, + // ?Version=this.Version, + // ?Comments=this.Comments + // ) let addButton (clickEvent: MouseEvent -> unit) = Html.div [ prop.classes ["is-flex"; "is-justify-content-center"] prop.children [ Bulma.button.button [ - prop.className "is-ghost" prop.text "+" prop.onClick clickEvent ] @@ -167,6 +288,133 @@ module Helper = ] ] + let readOnlyFormElement(v: string option, label: string) = + let v = defaultArg v "-" + Bulma.field.div [ + prop.className "is-flex is-flex-direction-column is-flex-grow-1" + prop.children [ + Bulma.label label + Bulma.control.div [ + Bulma.control.isExpanded + prop.children [ + Bulma.input.text [ + prop.readOnly true + prop.valueOrDefault v + ] + ] + ] + ] + ] + + let personModal (person: Person, confirm, back) = + Bulma.modal [ + Bulma.modal.isActive + prop.children [ + Bulma.modalBackground [] + Bulma.modalClose [] + Bulma.modalContent [ + Bulma.container [ + prop.className "p-1" + prop.children [ + Bulma.box [ + cardFormGroup [ + readOnlyFormElement(person.FirstName, "Given Name") + readOnlyFormElement(person.LastName, "Family Name") + ] + cardFormGroup [ + readOnlyFormElement(person.EMail, "Email") + readOnlyFormElement(person.ORCID, "ORCID") + ] + cardFormGroup [ + readOnlyFormElement(person.Affiliation, "Affiliation") + ] + Bulma.field.div [ + prop.className "is-flex is-justify-content-flex-end" + prop.style [style.gap (length.rem 1)] + prop.children [ + Bulma.button.button [ + prop.text "back" + prop.onClick back + ] + Bulma.button.button [ + Bulma.color.isSuccess + prop.text "confirm" + prop.onClick confirm + ] + ] + ] + ] + ] + ] + ] + ] + ] + + let publicationModal (pub: Publication, confirm, back) = + Bulma.modal [ + Bulma.modal.isActive + prop.children [ + Bulma.modalBackground [] + Bulma.modalClose [] + Bulma.modalContent [ + Bulma.container [ + prop.className "p-1" + prop.children [ + Bulma.box [ + cardFormGroup [ + readOnlyFormElement(pub.Title, "Title") + ] + cardFormGroup [ + readOnlyFormElement(pub.DOI, "DOI") + readOnlyFormElement(pub.PubMedID, "PubMedID") + ] + cardFormGroup [ + readOnlyFormElement(pub.Authors, "Authors") + ] + cardFormGroup [ + readOnlyFormElement(pub.Status |> Option.map _.ToString(), "Status") + ] + Bulma.field.div [ + prop.className "is-flex is-justify-content-flex-end" + prop.style [style.gap (length.rem 1)] + prop.children [ + Bulma.button.button [ + prop.text "back" + prop.onClick back + ] + Bulma.button.button [ + Bulma.color.isSuccess + prop.text "confirm" + prop.onClick confirm + ] + ] + ] + ] + ] + ] + ] + ] + ] + + let errorModal (error: exn, back) = + Bulma.modal [ + Bulma.modal.isActive + prop.children [ + Bulma.modalBackground [prop.onClick back] + Bulma.modalClose [prop.onClick back] + Bulma.modalContent [ + Bulma.notification [ + Bulma.color.isDanger + prop.children [ + Bulma.delete [prop.onClick back] + Html.div error.Message + ] + ] + ] + ] + ] + + type FormComponents = [] @@ -176,7 +424,7 @@ type FormComponents = let fullwidth = defaultArg fullwidth false let loading, setLoading = React.useState(false) let state, setState = React.useState(input) - let debounceStorage, setdebounceStorage = React.useState(newDebounceStorage) + let debounceStorage = React.useRef(newDebounceStorage()) React.useEffect((fun () -> setState input), dependencies=[|box input|]) Bulma.field.div [ prop.style [if fullwidth then style.flexGrow 1] @@ -193,7 +441,7 @@ type FormComponents = prop.valueOrDefault state prop.onChange(fun (e: string) -> setState e - debouncel debounceStorage label 1000 setLoading setter e + debouncel debounceStorage.current label 1000 setLoading setter e ) ] ] @@ -337,44 +585,179 @@ type FormComponents = ] [] - static member OntologyAnnotationInput (input: OntologyAnnotation, label: string, setter: OntologyAnnotation -> unit, ?showTextLabels: bool, ?removebutton: MouseEvent -> unit) = + static member PublicationRequestInput (id: string option,searchAPI: string -> Fable.Core.JS.Promise, doisetter, searchsetter: Publication -> unit, ?label:string) = + let id = defaultArg id "" + let state, setState = React.useState(API.Request.Idle) + let resetState = fun _ -> setState API.Request.Idle + Bulma.field.div [ + prop.className "is-flex-grow-1" + prop.children [ + if label.IsSome then Bulma.label label.Value + Bulma.field.div [ + Bulma.field.hasAddons + prop.children [ + //if state.IsSome || error.IsSome then + match state with + | API.Request.Ok pub -> Helper.publicationModal(pub,(fun _ -> searchsetter pub; resetState()), resetState) + | API.Request.Error e -> Helper.errorModal(e, resetState) + | API.Request.Loading -> Modals.Loading.loadingModal + | _ -> Html.none + Bulma.control.div [ + Bulma.control.isExpanded + prop.children [ + FormComponents.TextInput( + id, + "", + setter=doisetter, + fullwidth=true + ) + ] + ] + Bulma.control.div [ + Bulma.button.button [ + Bulma.color.isInfo + prop.text "Search" + prop.onClick (fun _ -> + //API.requestByORCID ("0000-0002-8510-6810") |> Promise.start + setState API.Request.Loading + API.start + searchAPI + id + (API.Request.Ok >> setState) + (API.Request.Error >> setState) + ) + ] + ] + ] + ] + ] + ] + + [] + static member PersonRequestInput (orcid: string option, doisetter, searchsetter: Person -> unit, ?label:string) = + let orcid = defaultArg orcid "" + let state, setState = React.useState(API.Request.Idle) + let resetState = fun _ -> setState API.Request.Idle + Bulma.field.div [ + prop.className "is-flex-grow-1" + prop.children [ + if label.IsSome then Bulma.label label.Value + Bulma.field.div [ + Bulma.field.hasAddons + prop.children [ + //if state.IsSome || error.IsSome then + match state with + | API.Request.Ok p -> Helper.personModal (p, (fun _ -> searchsetter p; resetState()), resetState) + | API.Request.Error e -> Helper.errorModal(e, resetState) + | API.Request.Loading -> Modals.Loading.loadingModal + | _ -> Html.none + Bulma.control.div [ + Bulma.control.isExpanded + prop.children [ + FormComponents.TextInput( + orcid, + "", + setter=doisetter, + fullwidth=true + ) + ] + ] + Bulma.control.div [ + Bulma.button.button [ + Bulma.color.isInfo + prop.text "Search" + prop.onClick (fun _ -> + //API.requestByORCID ("0000-0002-8510-6810") |> Promise.start + setState API.Request.Loading + API.start + API.requestByORCID + orcid + (API.Request.Ok >> setState) + (API.Request.Error >> setState) + ) + ] + ] + ] + ] + ] + ] + + [] + static member DOIInput (id: string option, doisetter, searchsetter: Publication -> unit, ?label:string) = + FormComponents.PublicationRequestInput( + id, + API.requestByDOI,//"10.3390/ijms24087444"//"10.3390/ijms2408741d"// + doisetter, + searchsetter, + ?label=label + ) + + [] + static member PubMedIDInput (id: string option, doisetter, searchsetter: Publication -> unit, ?label:string) = + FormComponents.PublicationRequestInput( + id, + API.requestByPubMedID, + doisetter, + searchsetter, + ?label=label + ) + + [] + static member OntologyAnnotationInput (input: OntologyAnnotation, setter: OntologyAnnotation -> unit, ?label: string, ?showTextLabels: bool, ?removebutton: MouseEvent -> unit, ?parent: OntologyAnnotation) = let showTextLabels = defaultArg showTextLabels true - let state, setState = React.useState(Helper.OntologyAnnotationMutable.fromOntologyAnnotation input) - React.useEffect((fun () -> setState <| Helper.OntologyAnnotationMutable.fromOntologyAnnotation input), dependencies=[|box input|]) - let hasLabel = label <> "" + let state, setState = React.useState(input) + let element = React.useElementRef() + React.useEffect((fun () -> setState input), dependencies=[|box input|]) Bulma.field.div [ - if hasLabel then Bulma.label label + //if label.IsSome then Bulma.label label.Value Bulma.field.div [ + //prop.ref element + prop.style [style.position.relative] prop.classes ["is-flex"; "is-flex-direction-row"; "is-justify-content-space-between"] prop.children [ Html.div [ prop.classes ["form-container"; if removebutton.IsSome then "pr-2"] prop.children [ + Bulma.field.div [ + prop.style [style.flexGrow 1] + prop.children [ + let label = defaultArg label "Term Name" + Bulma.label label + let innersetter = + fun (oaOpt: OntologyAnnotation option) -> + if oaOpt.IsSome then + setter oaOpt.Value + setState oaOpt.Value + Components.TermSearch.Input( + innersetter, + input=state, + fullwidth=true, + ?portalTermSelectArea=element.current, + ?parent=parent, + debounceSetter=1000 + ) + ] + ] + Html.div [ + prop.classes ["form-input-term-search-positioner"] + prop.ref element + ] FormComponents.TextInput( - Option.defaultValue "" state.Name, - (if showTextLabels then $"Term Name" else ""), - (fun s -> - let s = if s = "" then None else Some s - state.Name <- s - state.ToOntologyAnnotation() |> setter), - fullwidth = true - ) - FormComponents.TextInput( - Option.defaultValue "" state.TSR, + Option.defaultValue "" state.TermSourceREF, (if showTextLabels then $"TSR" else ""), (fun s -> let s = if s = "" then None else Some s - state.TSR <- s - state.ToOntologyAnnotation() |> setter), + state.TermSourceREF <- s + state |> setter), fullwidth = true ) FormComponents.TextInput( - Option.defaultValue "" state.TAN, + Option.defaultValue "" state.TermAccessionNumber, (if showTextLabels then $"TAN" else ""), (fun s -> let s = if s = "" then None else Some s - state.TAN <- s - state.ToOntologyAnnotation() |> setter), + state.TermAccessionNumber <- s + state |> setter), fullwidth = true ) ] @@ -394,18 +777,18 @@ type FormComponents = ] [] - static member OntologyAnnotationsInput (oas: OntologyAnnotation [], label: string, setter: OntologyAnnotation [] -> unit, ?showTextLabels: bool) = + static member OntologyAnnotationsInput (oas: OntologyAnnotation [], label: string, setter: OntologyAnnotation [] -> unit, ?showTextLabels: bool, ?parent: OntologyAnnotation) = FormComponents.InputSequence( - oas, OntologyAnnotation.empty, label, setter, - (fun (a,b,c,d) -> FormComponents.OntologyAnnotationInput(a,b,c,removebutton=d,?showTextLabels=showTextLabels)) + oas, (OntologyAnnotation.empty()), label, setter, + (fun (a,b,c,d) -> FormComponents.OntologyAnnotationInput(a,c,label=b,removebutton=d,?showTextLabels=showTextLabels, ?parent=parent)) ) [] static member PersonInput(input: Person, setter: Person -> unit, ?deletebutton: MouseEvent -> unit) = let isExtended, setIsExtended = React.useState(false) // Must use `React.useRef` do this. Otherwise simultanios updates will overwrite each other - let state, setState = React.useState(Helper.PersonMutable.fromPerson input) - React.useEffect((fun _ -> setState <| Helper.PersonMutable.fromPerson input), [|box input|]) + let state, setState = React.useState(input) + React.useEffect((fun _ -> setState input), [|box input|]) let fn = Option.defaultValue "" state.FirstName let ln = Option.defaultValue "" state.LastName let mi = Option.defaultValue "" state.MidInitials @@ -420,21 +803,21 @@ type FormComponents = (fun s -> let s = if s = "" then None else Some s personSetter s - state.ToPerson() |> setter), + state |> setter), fullwidth=true ) - let countFilledFieldsString (person: Helper.PersonMutable) = + let countFilledFieldsString (person: Person) = let fields = [ - state.FirstName - state.LastName - state.MidInitials - state.ORCID - state.Address - state.Affiliation - state.EMail - state.Phone - state.Fax - state.Roles |> Option.map (fun _ -> "") + person.FirstName + person.LastName + person.MidInitials + person.ORCID + person.Address + person.Affiliation + person.EMail + person.Phone + person.Fax + if person.Roles.Count > 0 then Some "roles" else None // just for count. Value does not matter ] let all = fields.Length let filled = fields |> List.choose id |> _.Length @@ -470,7 +853,15 @@ type FormComponents = ] Helper.cardFormGroup [ createPersonFieldTextInput(state.MidInitials, "Mid Initials", fun s -> state.MidInitials <- s) - createPersonFieldTextInput(state.ORCID, "ORCID", fun s -> state.ORCID <- s) + FormComponents.PersonRequestInput( + state.ORCID, + (fun s -> + let s = if s = "" then None else Some s + state.ORCID <- s + state |> setter), + (fun s -> setter s), + "ORCID" + ) ] Helper.cardFormGroup [ createPersonFieldTextInput(state.Affiliation, "Affiliation", fun s -> state.Affiliation <- s) @@ -482,14 +873,14 @@ type FormComponents = createPersonFieldTextInput(state.Fax, "Fax", fun s -> state.Fax <- s) ] FormComponents.OntologyAnnotationsInput( - Option.defaultValue [||] state.Roles, + Array.ofSeq state.Roles, "Roles", (fun oas -> - let oas = if oas = [||] then None else Some oas - state.Roles <- oas - state.ToPerson() |> setter + state.Roles <- ResizeArray(oas) + state |> setter ), showTextLabels = false + //parent=Shared.TermCollection.PersonRoleWithinExperiment ) if deletebutton.IsSome then Helper.deleteButton deletebutton.Value @@ -527,13 +918,17 @@ type FormComponents = FormComponents.TextInput( comment.Name |> Option.defaultValue "", (if showTextLabels then $"Term Name" else ""), - (fun s -> {comment with Name = if s = "" then None else Some s} |> setter), + (fun s -> + comment.Name <- if s = "" then None else Some s + comment |> setter), fullwidth = true ) FormComponents.TextInput( comment.Value |> Option.defaultValue "", (if showTextLabels then $"TSR" else ""), - (fun s -> {comment with Value = if s = "" then None else Some s} |> setter), + (fun s -> + comment.Value <- if s = "" then None else Some s + comment |> setter), fullwidth = true ) if removebutton.IsSome then @@ -570,27 +965,26 @@ type FormComponents = static member PublicationInput(input: Publication, setter: Publication -> unit, ?deletebutton: MouseEvent -> unit) = let isExtended, setIsExtended = React.useState(false) // Must use `React.useRef` do this. Otherwise simultanios updates will overwrite each other - let state, setState = React.useState(Helper.PublicationMutable.fromPublication input) - React.useEffect((fun _ -> setState <| Helper.PublicationMutable.fromPublication input), [|box input|]) + let state, setState = React.useState(input) + React.useEffect((fun _ -> setState input), [|box input|]) let title = Option.defaultValue "" state.Title - let doi = Option.defaultValue "<doi>" state.Doi - let createPersonFieldTextInput(field: string option, label, personSetter: string option -> unit) = + let doi = Option.defaultValue "<doi>" state.DOI + let createPersonFieldTextInput(field: string option, label, publicationSetter: string option -> unit) = FormComponents.TextInput( field |> Option.defaultValue "", label, (fun s -> let s = if s = "" then None else Some s - personSetter s - state.ToPublication() |> setter), + publicationSetter s + state |> setter), fullwidth=true ) let countFilledFieldsString () = let fields = [ - state.PubmedId - state.Doi + state.PubMedID + state.DOI state.Title state.Authors - state.Comments |> Option.map (fun _ -> "") state.Status |> Option.map (fun _ -> "") ] let all = fields.Length @@ -623,24 +1017,41 @@ type FormComponents = prop.children [ createPersonFieldTextInput(state.Title, "Title", fun s -> state.Title <- s) Helper.cardFormGroup [ - createPersonFieldTextInput(state.PubmedId, "PubMed Id", fun s -> state.PubmedId <- s) - createPersonFieldTextInput(state.Doi, "DOI", fun s -> state.Doi <- s) + FormComponents.PubMedIDInput( + state.PubMedID, + (fun s -> + let s = if s = "" then None else Some s + state.PubMedID <- s + state |> setter), + (fun pub -> setter pub), + "PubMed Id" + ) + FormComponents.DOIInput( + state.DOI, + (fun s -> + let s = if s = "" then None else Some s + state.DOI <- s + state |> setter), + (fun pub -> setter pub), + "DOI" + ) ] createPersonFieldTextInput(state.Authors, "Authors", fun s -> state.Authors <- s) FormComponents.OntologyAnnotationInput( - Option.defaultValue OntologyAnnotation.empty state.Status, - "Status", + Option.defaultValue (OntologyAnnotation.empty()) state.Status, (fun s -> - state.Status <- if s = OntologyAnnotation.empty then None else Some s - state.ToPublication() |> setter - ) + state.Status <- if s = (OntologyAnnotation.empty()) then None else Some s + state |> setter + ), + "Status", + parent=Shared.TermCollection.PublicationStatus ) FormComponents.CommentsInput( - Option.defaultValue [||] state.Comments, + Array.ofSeq state.Comments, "Comments", (fun c -> - state.Comments <- if c = [||] then None else Some c - state.ToPublication() |> setter + state.Comments <- ResizeArray(c) + state |> setter ) ) if deletebutton.IsSome then @@ -658,96 +1069,12 @@ type FormComponents = (fun (a,b,c,d) -> FormComponents.PublicationInput(a,c,deletebutton=d)) ) - [<ReactComponent>] - static member FactorInput(input: Factor, setter: Factor -> unit, ?deletebutton: MouseEvent -> unit) = - let isExtended, setIsExtended = React.useState(false) - // Must use `React.useRef` do this. Otherwise simultanios updates will overwrite each other - let state, setState = React.useState(Helper.FactorMutable.fromFactor input) - React.useEffect((fun _ -> setState <| Helper.FactorMutable.fromFactor input), [|box input|]) - let name = Option.defaultValue "<name>" state.Name - let type' = Option.defaultValue "<type>" (state.FactorType |> Option.map (fun x -> x.NameText)) - let createFieldTextInput(field: string option, label, personSetter: string option -> unit) = - FormComponents.TextInput( - field |> Option.defaultValue "", - label, - (fun s -> - let s = if s = "" then None else Some s - personSetter s - state.ToFactor() |> setter), - fullwidth=true - ) - let countFilledFieldsString () = - let fields = [ - state.Name - state.FactorType |> Option.map (fun _ -> "") - state.Comments |> Option.map (fun _ -> "") - ] - let all = fields.Length - let filled = fields |> List.choose id |> _.Length - $"{filled}/{all}" - Bulma.card [ - Bulma.cardHeader [ - Bulma.cardHeaderTitle.div [ - //prop.classes ["is-align-items-flex-start"] - prop.children [ - Html.div [ - Bulma.title.h5 name - Bulma.subtitle.h6 type' - ] - Html.div [ - prop.style [style.custom("marginLeft", "auto")] - prop.text (countFilledFieldsString ()) - ] - ] - ] - Bulma.cardHeaderIcon.a [ - prop.onClick (fun _ -> not isExtended |> setIsExtended) - prop.children [ - Bulma.icon [Html.i [prop.classes ["fas"; "fa-angle-down"]]] - ] - ] - ] - Bulma.cardContent [ - prop.classes [if not isExtended then "is-hidden"] - prop.children [ - createFieldTextInput(state.Name, "Name", fun s -> state.Name <- s) - FormComponents.OntologyAnnotationInput( - Option.defaultValue OntologyAnnotation.empty state.FactorType, - "Status", - (fun s -> - state.FactorType <- if s = OntologyAnnotation.empty then None else Some s - state.ToFactor() |> setter - ) - ) - FormComponents.CommentsInput( - Option.defaultValue [||] state.Comments, - "Comments", - (fun c -> - state.Comments <- if c = [||] then None else Some c - state.ToFactor() |> setter - ) - ) - if deletebutton.IsSome then - Helper.deleteButton deletebutton.Value - ] - ] - ] - - static member FactorsInput(input: Factor [], label: string, setter: Factor [] -> unit) = - FormComponents.InputSequence( - input, - Factor.create(), - label, - setter, - (fun (a,b,c,d) -> FormComponents.FactorInput(a,c,deletebutton=d)) - ) - [<ReactComponent>] static member OntologySourceReferenceInput(input: OntologySourceReference, setter: OntologySourceReference -> unit, ?deletebutton: MouseEvent -> unit) = let isExtended, setIsExtended = React.useState(false) // Must use `React.useRef` do this. Otherwise simultanios updates will overwrite each other - let state, setState = React.useState(Helper.OntologySourceReferenceMutable.fromOntologySourceReference input) - React.useEffect((fun _ -> setState <| Helper.OntologySourceReferenceMutable.fromOntologySourceReference input), [|box input|]) + let state, setState = React.useState(input) + React.useEffect((fun _ -> setState input), [|box input|]) let name = Option.defaultValue "<name>" state.Name let version = Option.defaultValue "<version>" state.Version let createFieldTextInput(field: string option, label, personSetter: string option -> unit) = @@ -757,7 +1084,7 @@ type FormComponents = (fun s -> let s = if s = "" then None else Some s personSetter s - state.ToOntologySourceReference() |> setter), + state |> setter), fullwidth=true ) let countFilledFieldsString () = @@ -766,7 +1093,7 @@ type FormComponents = state.File state.Version state.Description - state.Comments |> Option.map (fun _ -> "") + if state.Comments.Count > 0 then Some "comments" else None // just for count. Value does not matter ] let all = fields.Length let filled = fields |> List.choose id |> _.Length @@ -807,16 +1134,16 @@ type FormComponents = (fun s -> let s = if s = "" then None else Some s state.Description <- s - state.ToOntologySourceReference() |> setter), + state |> setter), fullwidth=true, isarea=true ) FormComponents.CommentsInput( - Option.defaultValue [||] state.Comments, + Array.ofSeq state.Comments, "Comments", (fun c -> - state.Comments <- if c = [||] then None else Some c - state.ToOntologySourceReference() |> setter + state.Comments <- ResizeArray(c) + state |> setter ) ) if deletebutton.IsSome then diff --git a/src/Client/MainComponents/Metadata/Investigation.fs b/src/Client/MainComponents/Metadata/Investigation.fs index 6823884d..671c8265 100644 --- a/src/Client/MainComponents/Metadata/Investigation.fs +++ b/src/Client/MainComponents/Metadata/Investigation.fs @@ -1,4 +1,4 @@ -module MainComponents.Metadata.Investigation +module MainComponents.Metadata.Investigation open Feliz open Feliz.Bulma @@ -7,7 +7,7 @@ open Spreadsheet open Messages open Browser.Types open Fable.Core.JsInterop -open ARCtrl.ISA +open ARCtrl open Shared let Main(inv: ArcInvestigation, model: Messages.Model, dispatch: Msg -> unit) = @@ -37,6 +37,20 @@ let Main(inv: ArcInvestigation, model: Messages.Model, dispatch: Msg -> unit) = fullwidth=true, isarea=true ) + FormComponents.PersonsInput( + Array.ofSeq inv.Contacts, + "Contacts", + (fun i -> + inv.Contacts <- ResizeArray i + inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) + ) + FormComponents.PublicationsInput( + Array.ofSeq inv.Publications, + "Publications", + (fun i -> + inv.Publications <- ResizeArray i + inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) + ) FormComponents.DateTimeInput ( Option.defaultValue "" inv.SubmissionDate, "Submission Date", @@ -52,38 +66,24 @@ let Main(inv: ArcInvestigation, model: Messages.Model, dispatch: Msg -> unit) = inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) ) FormComponents.OntologySourceReferencesInput( - inv.OntologySourceReferences, + Array.ofSeq inv.OntologySourceReferences, "Ontology Source References", (fun oas -> - inv.OntologySourceReferences <- oas - inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) - ) - FormComponents.PublicationsInput( - inv.Publications, - "Publications", - (fun i -> - inv.Publications <- i - inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) - ) - FormComponents.PersonsInput( - inv.Contacts, - "Contacts", - (fun i -> - inv.Contacts <- i - inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) - ) - FormComponents.TextInputs( - Array.ofSeq inv.RegisteredStudyIdentifiers, - "RegisteredStudyIdentifiers", - (fun i -> - inv.RegisteredStudyIdentifiers <- ResizeArray i + inv.OntologySourceReferences <- ResizeArray oas inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) ) + //FormComponents.TextInputs( + // Array.ofSeq inv.RegisteredStudyIdentifiers, + // "RegisteredStudyIdentifiers", + // (fun i -> + // inv.RegisteredStudyIdentifiers <- ResizeArray i + // inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) + //) FormComponents.CommentsInput( - inv.Comments, + Array.ofSeq inv.Comments, "Comments", (fun i -> - inv.Comments <- i + inv.Comments <- ResizeArray i inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) ) ] \ No newline at end of file diff --git a/src/Client/MainComponents/Metadata/Study.fs b/src/Client/MainComponents/Metadata/Study.fs index 4cce4c12..cec36820 100644 --- a/src/Client/MainComponents/Metadata/Study.fs +++ b/src/Client/MainComponents/Metadata/Study.fs @@ -1,9 +1,9 @@ -module MainComponents.Metadata.Study +module MainComponents.Metadata.Study open Feliz open Feliz.Bulma open Messages -open ARCtrl.ISA +open ARCtrl open Shared let Main(study: ArcStudy, assignedAssays: ArcAssay list, model: Messages.Model, dispatch: Msg -> unit) = @@ -26,6 +26,20 @@ let Main(study: ArcStudy, assignedAssays: ArcAssay list, model: Messages.Model, fullwidth=true, isarea=true ) + FormComponents.PersonsInput( + Array.ofSeq study.Contacts, + "Contacts", + fun persons -> + study.Contacts <- ResizeArray(persons) + (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + ) + FormComponents.PublicationsInput ( + Array.ofSeq study.Publications, + "Publications", + fun pubs -> + study.Publications <- ResizeArray(pubs) + (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + ) FormComponents.DateTimeInput( Option.defaultValue "" study.SubmissionDate, "Submission Date", @@ -42,46 +56,25 @@ let Main(study: ArcStudy, assignedAssays: ArcAssay list, model: Messages.Model, study.PublicReleaseDate <- s (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch ) - FormComponents.PublicationsInput ( - study.Publications, - "Publications", - fun pubs -> - study.Publications <- pubs - (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch - ) - FormComponents.PersonsInput( - study.Contacts, - "Contacts", - fun persons -> - study.Contacts <- persons - (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch - ) FormComponents.OntologyAnnotationsInput( - study.StudyDesignDescriptors, + Array.ofSeq study.StudyDesignDescriptors, "Study Design Descriptors", fun oas -> - study.StudyDesignDescriptors <- oas - (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch - ) - FormComponents.TextInputs( - Array.ofSeq study.RegisteredAssayIdentifiers, - "Registered Assay Identifiers", - fun rais -> - study.RegisteredAssayIdentifiers <- ResizeArray(rais) - (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch - ) - FormComponents.FactorsInput( - study.Factors, - "Factors", - fun factors -> - study.Factors <- factors + study.StudyDesignDescriptors <- ResizeArray(oas) (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch ) + //FormComponents.TextInputs( + // Array.ofSeq study.RegisteredAssayIdentifiers, + // "Registered Assay Identifiers", + // fun rais -> + // study.RegisteredAssayIdentifiers <- ResizeArray(rais) + // (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + //) FormComponents.CommentsInput( - study.Comments, + Array.ofSeq study.Comments, "Comments", fun comments -> - study.Comments <- comments + study.Comments <- ResizeArray(comments) (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch ) ] \ No newline at end of file diff --git a/src/Client/MainComponents/Metadata/Template.fs b/src/Client/MainComponents/Metadata/Template.fs index 28ec2d58..cbecc012 100644 --- a/src/Client/MainComponents/Metadata/Template.fs +++ b/src/Client/MainComponents/Metadata/Template.fs @@ -7,7 +7,7 @@ open Spreadsheet open Messages open Browser.Types open Fable.Core.JsInterop -open ARCtrl.ISA +open ARCtrl open Shared open ARCtrl.Template @@ -63,24 +63,24 @@ let Main(template: Template, model: Messages.Model, dispatch: Msg -> unit) = template |> ArcFiles.Template |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) ) FormComponents.OntologyAnnotationsInput( - template.Tags, + Array.ofSeq template.Tags, "Tags", (fun (s) -> - template.Tags <- s + template.Tags <- ResizeArray s template |> ArcFiles.Template |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) ) FormComponents.OntologyAnnotationsInput( - template.EndpointRepositories, + Array.ofSeq template.EndpointRepositories, "Endpoint Repositories", (fun (s) -> - template.EndpointRepositories <- s + template.EndpointRepositories <- ResizeArray s template |> ArcFiles.Template |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) ) FormComponents.PersonsInput( - template.Authors, + Array.ofSeq template.Authors, "Authors", (fun (s) -> - template.Authors <- s + template.Authors <-ResizeArray s template |> ArcFiles.Template |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) ) ] \ No newline at end of file diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index 1f897290..419f0e0b 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -7,9 +7,9 @@ open Feliz.Bulma open LocalHistory open Messages open Components.QuickAccessButton +open MainComponents - -let quickAccessButtonListStart (state: LocalHistory.Model) dispatch = +let private quickAccessButtonListStart (state: LocalHistory.Model) dispatch = Html.div [ prop.style [ style.display.flex; style.flexDirection.row @@ -47,14 +47,14 @@ let quickAccessButtonListStart (state: LocalHistory.Model) dispatch = ] ] -let quickAccessButtonListEnd (model: Model) dispatch = +let private quickAccessButtonListEnd (model: Model) dispatch = Html.div [ prop.style [ style.display.flex; style.flexDirection.row ] prop.children [ QuickAccessButton.create( - "Save as xlsx", + "Save", [ Bulma.icon [Html.i [prop.className "fa-solid fa-floppy-disk";]] ], @@ -66,14 +66,60 @@ let quickAccessButtonListEnd (model: Model) dispatch = Bulma.icon [Html.i [prop.className "fa-sharp fa-solid fa-trash";]] ], (fun _ -> Modals.Controller.renderModal("ResetTableWarning", Modals.ResetTable.Main dispatch)), - buttonProps = [Bulma.color.isDanger; Bulma.button.isInverted; Bulma.button.isOutlined] + buttonProps = [Bulma.color.isDanger] + ).toReactElement() + ] + ] + +let private WidgetNavbarList (model, dispatch, addWidget: Widget -> unit) = + Html.div [ + prop.style [ + style.display.flex; style.flexDirection.row + ] + prop.children [ + QuickAccessButton.create( + "Add Building Block", + [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table-columns" ] + ] + ], + (fun _ -> addWidget Widget._BuildingBlock) + ).toReactElement() + QuickAccessButton.create( + "Add Template", + [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table" ] + ] + ], + (fun _ -> addWidget Widget._Template) + ).toReactElement() + QuickAccessButton.create( + "File Picker", + [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-file-signature" ] + ] + ], + (fun _ -> addWidget Widget._FilePicker) ).toReactElement() ] ] + [<ReactComponent>] -let Main (model: Messages.Model) dispatch = +let Main(model: Messages.Model, dispatch, widgets, setWidgets) = + let addWidget (widget: Widget) = + let add (widget) widgets = widget::widgets |> List.rev |> setWidgets + if widgets |> List.contains widget then + List.filter (fun w -> w <> widget) widgets + |> fun filteredWidgets -> add widget filteredWidgets + else + add widget widgets Bulma.navbar [ prop.className "myNavbarSticky" prop.id "swate-mainNavbar" @@ -95,32 +141,12 @@ let Main (model: Messages.Model) dispatch = prop.ariaLabel "menu" prop.children [ match model.PersistentStorageState.Host with - | Some Swatehost.ARCitect -> + | Some (Swatehost.ARCitect) -> Bulma.navbarStart.div [ prop.style [style.display.flex; style.alignItems.stretch; style.justifyContent.flexStart; style.custom("marginRight", "auto")] prop.children [ - Html.div [ - prop.style [ - style.display.flex; style.flexDirection.row - ] - prop.children [ - QuickAccessButton.create( - "Return to ARCitect", - [ - Bulma.icon [Html.i [prop.className "fa-solid fa-circle-left";]] - ], - (fun _ -> ARCitect.ARCitect.send Model.ARCitect.TriggerSwateClose) - ).toReactElement() - QuickAccessButton.create( - "Alpha State", - [ - Html.span "ALPHA STATE" - ], - (fun e -> ()), - false - ).toReactElement() - ] - ] + quickAccessButtonListStart model.History dispatch + if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, addWidget) ] ] | Some _ -> @@ -128,6 +154,7 @@ let Main (model: Messages.Model) dispatch = prop.style [style.display.flex; style.alignItems.stretch; style.justifyContent.flexStart; style.custom("marginRight", "auto")] prop.children [ quickAccessButtonListStart model.History dispatch + if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, addWidget) ] ] Bulma.navbarEnd.div [ diff --git a/src/Client/MainComponents/NoTablesElement.fs b/src/Client/MainComponents/NoTablesElement.fs index 4cd4c1de..4a1e82e1 100644 --- a/src/Client/MainComponents/NoTablesElement.fs +++ b/src/Client/MainComponents/NoTablesElement.fs @@ -3,11 +3,11 @@ module MainComponents.NoTablesElement open Feliz open Feliz.Bulma -open Spreadsheet +open SpreadsheetInterface open Messages open Browser.Types open Fable.Core.JsInterop -open ARCtrl.ISA +open ARCtrl open Shared open Elmish @@ -22,7 +22,7 @@ module private UploadHandler = [<Literal>] let id = "droparea" - let updateMsg = fun r -> r |> SetArcFileFromBytes |> SpreadsheetMsg + let updateMsg = fun r -> r |> ImportXlsx |> InterfaceMsg let setActive_DropArea() = styleCounter <- styleCounter + 1 @@ -69,7 +69,7 @@ let private uploadNewTable dispatch = reader.onload <- fun evt -> let (r: byte []) = evt.target?result - r |> SetArcFileFromBytes |> SpreadsheetMsg |> dispatch + r |> ImportXlsx |> InterfaceMsg |> dispatch reader.onerror <- fun evt -> curry GenericLog Cmd.none ("Error", evt?Value) |> DevMsg |> dispatch @@ -127,7 +127,7 @@ let private createNewTable isActive toggle (dispatch: Messages.Msg -> unit) = let i = ArcInvestigation.init("New Investigation") ArcFiles.Investigation i |> UpdateArcFile - |> Messages.SpreadsheetMsg + |> InterfaceMsg |> dispatch ) prop.text "Investigation" @@ -135,10 +135,10 @@ let private createNewTable isActive toggle (dispatch: Messages.Msg -> unit) = Bulma.dropdownItem.a [ prop.onClick(fun _ -> let s = ArcStudy.init("New Study") - let newTable = s.InitTable("New Study Table") + let _ = s.InitTable("New Study Table") ArcFiles.Study (s, []) |> UpdateArcFile - |> Messages.SpreadsheetMsg + |> InterfaceMsg |> dispatch ) prop.text "Study" @@ -149,7 +149,7 @@ let private createNewTable isActive toggle (dispatch: Messages.Msg -> unit) = let newTable = a.InitTable("New Assay Table") ArcFiles.Assay a |> UpdateArcFile - |> Messages.SpreadsheetMsg + |> InterfaceMsg |> dispatch ) prop.text "Assay" @@ -165,7 +165,7 @@ let private createNewTable isActive toggle (dispatch: Messages.Msg -> unit) = template.LastUpdated <- System.DateTime.Now ArcFiles.Template template |> UpdateArcFile - |> Messages.SpreadsheetMsg + |> InterfaceMsg |> dispatch ) prop.text "Template" diff --git a/src/Client/MainComponents/SpreadsheetView.fs b/src/Client/MainComponents/SpreadsheetView.fs index 0831d964..1c98ad91 100644 --- a/src/Client/MainComponents/SpreadsheetView.fs +++ b/src/Client/MainComponents/SpreadsheetView.fs @@ -6,22 +6,10 @@ open Feliz.Bulma open Spreadsheet open Messages open Spreadsheet.Cells -open ARCtrl.ISA +open ARCtrl open Shared - -//let private referenceColumns (state:Set<int>, header:SwateCell, (columnIndex: int, rowIndex:int), model, dispatch) = -// if header.Header.isTermColumn then -// [ -// let isExtended = state.Contains(columnIndex) -// if isExtended then -// if header.Header.HasUnit then -// yield UnitCell((columnIndex,rowIndex), model, dispatch) -// yield TANCell((columnIndex,rowIndex), model, dispatch) -// ] -// else [] - -let cellPlaceholder (c_opt: CompositeCell option) = +let private cellPlaceholder (c_opt: CompositeCell option) = let tableCell (children: ReactElement list) = Html.td [ Html.div [ prop.style [style.minHeight (length.px 30); style.minWidth (length.px 100)] @@ -40,38 +28,92 @@ let cellPlaceholder (c_opt: CompositeCell option) = ] ] -let private bodyRow (rowIndex: int) (state:Set<int>) setState (model:Model) (dispatch: Msg -> unit) = +/// <summary> +/// rowIndex < 0 equals header +/// </summary> +/// <param name="rowIndex"></param> +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<int>) (model:Model) (dispatch: Msg -> unit) = let table = model.SpreadsheetModel.ActiveTable Html.tr [ + RowLabel rowIndex for columnIndex in 0 .. (table.ColumnCount-1) do let index = columnIndex, rowIndex - Cells.BodyCell(index, state, setState, model, dispatch) - //Cell((columnIndex,rowIndex), state, setState, model, dispatch) - //yield! referenceColumns(state, header, (column,row), model, dispatch) + let cell = model.SpreadsheetModel.ActiveTable.Values.[index] + Cells.Cell.Body (index, cell, model, dispatch) + let isExtended = state.Contains columnIndex + if isExtended && (cell.isTerm || cell.isUnitized) then + if cell.isUnitized then + Cell.BodyUnit(index, cell, model, dispatch) + else + Cell.Empty() + Cell.BodyTSR(index, cell, model, dispatch) + Cell.BodyTAN(index, cell, model, dispatch) ] -let private bodyRows (state:Set<int>) setState (model:Model) (dispatch: Msg -> unit) = +let private bodyRows (state:Set<int>) (model:Model) (dispatch: Msg -> unit) = Html.tbody [ for rowInd in 0 .. model.SpreadsheetModel.ActiveTable.RowCount-1 do - yield bodyRow rowInd state setState model dispatch + yield bodyRow rowInd state model dispatch ] let private headerRow (state:Set<int>) setState (model:Model) (dispatch: Msg -> unit) = let table = model.SpreadsheetModel.ActiveTable Html.tr [ + if table.ColumnCount > 0 then RowLabel -1 for columnIndex in 0 .. (table.ColumnCount-1) do - yield - Cells.HeaderCell(columnIndex, state, setState, model, dispatch) - //yield! referenceColumns(state, cell, (column,row), model, dispatch) + let header = table.Headers.[columnIndex] + Cells.Cell.Header(columnIndex, header, state, setState, model, dispatch) + let isExtended = state.Contains columnIndex + if isExtended then + Cell.HeaderUnit(columnIndex, header, state, setState, model, dispatch) + Cell.HeaderTSR(columnIndex, header, state, setState, model, dispatch) + Cell.HeaderTAN(columnIndex, header, state, setState, model, dispatch) ] +open Fable.Core.JsInterop + [<ReactComponent>] let Main (model:Model) (dispatch: Msg -> unit) = + //React.useListener.on("keydown", (Spreadsheet.KeyboardShortcuts.onKeydownEvent dispatch)) + let ref = React.useElementRef() + //React.useElementListener.on(ref, "keydown", (Spreadsheet.KeyboardShortcuts.onKeydownEvent dispatch)) /// This state is used to track which columns are expanded let state, setState : Set<int> * (Set<int> -> unit) = React.useState(Set.empty) + React.useEffect((fun _ -> setState Set.empty), [|box model.SpreadsheetModel.ActiveView|]) Html.div [ + prop.id "SPREADSHEET_MAIN_VIEW" + prop.tabIndex 0 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" @@ -79,7 +121,7 @@ let Main (model:Model) (dispatch: Msg -> unit) = Html.thead [ headerRow state setState model dispatch ] - bodyRows state setState model dispatch + bodyRows state model dispatch ] ] ] diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs new file mode 100644 index 00000000..21a5b542 --- /dev/null +++ b/src/Client/MainComponents/Widgets.fs @@ -0,0 +1,255 @@ +namespace MainComponents + +open Feliz +open Feliz.Bulma +open Browser.Types + +open LocalStorage.Widgets + +module private InitExtensions = + + type Rect with + + static member initSizeFromPrefix(prefix: string) = + match Size.load prefix with + | Some p -> Some p + | None -> None + + static member initPositionFromPrefix(prefix: string) = + match Position.load prefix with + | Some p -> Some p + | None -> None + + +open InitExtensions + +open Fable.Core +open Fable.Core.JsInterop +open Protocol + +module private MoveEventListener = + + open Fable.Core.JsInterop + + let ensurePositionInsideWindow (element:IRefValue<HTMLElement option>) (position: Rect) = + let maxX = Browser.Dom.window.innerWidth - element.current.Value.offsetWidth; + let tempX = position.X + let newX = System.Math.Min(System.Math.Max(tempX,0),int maxX) + let maxY = Browser.Dom.window.innerHeight - element.current.Value.offsetHeight; + let tempY = position.Y + let newY = System.Math.Min(System.Math.Max(tempY,0),int maxY) + {X = newX; Y = newY} + + let calculatePosition (element:IRefValue<HTMLElement option>) (startPosition: Rect) = fun (e: Event) -> + let e : MouseEvent = !!e + let tempX = int e.clientX - startPosition.X + let tempY = int e.clientY - startPosition.Y + let tempPosition = {X = tempX; Y = tempY} + ensurePositionInsideWindow element tempPosition + + let onmousemove (element:IRefValue<HTMLElement option>) (startPosition: Rect) setPosition = fun (e: Event) -> + let nextPosition = calculatePosition element startPosition e + setPosition (Some nextPosition) + + let onmouseup (prefix,element:IRefValue<HTMLElement option>) onmousemove = + Browser.Dom.document.removeEventListener("mousemove", onmousemove) + if element.current.IsSome then + let rect = element.current.Value.getBoundingClientRect() + let position = {X = int rect.left; Y = int rect.top} + Position.write(prefix,position) + +module private ResizeEventListener = + + open Fable.Core.JsInterop + + let onmousemove (startPosition: Rect) (startSize: Rect) setSize = fun (e: Event) -> + let e : MouseEvent = !!e + let width = int e.clientX - startPosition.X + startSize.X + // I did not enable this, as it creates issues with overlays such as the term search dropdown. + // The widget card itself has overflow: visible, which makes a set height impossible, + // but wihout the visible overflow term search results might require scrolling. + // // let height = int e.clientY - startPosition.Y + startSize.Y + setSize (Some {X = width; Y = startSize.Y}) + + let onmouseup (prefix, element: IRefValue<HTMLElement option>) onmousemove = + Browser.Dom.document.removeEventListener("mousemove", onmousemove) + if element.current.IsSome then + Size.write(prefix,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) + +module private Elements = + + let helpExtendButton (extendToggle: unit -> unit) = + Bulma.help [ + prop.className "is-flex" + prop.children [ + Html.a [ + prop.text "Help"; + prop.style [style.marginLeft length.auto; style.userSelect.none] + prop.onClick (fun e -> e.preventDefault(); e.stopPropagation(); extendToggle()) + ] + ] + ] + +[<RequireQualifiedAccess>] +type Widget = + | _BuildingBlock + | _Template + | _FilePicker + + [<ReactComponent>] + static member Base(content: ReactElement, prefix: string, rmv: MouseEvent -> unit, ?help: ReactElement) = + let position, setPosition = React.useState(fun _ -> Rect.initPositionFromPrefix prefix) + let size, setSize = React.useState(fun _ -> Rect.initSizeFromPrefix prefix) + let helpIsActive, setHelpIsActive = React.useState(false) + let element = React.useElementRef() + React.useLayoutEffectOnce(fun _ -> position |> Option.iter (fun position -> MoveEventListener.ensurePositionInsideWindow element position |> Some |> setPosition)) // Reposition widget inside window + let resizeElement (content: ReactElement) = + Bulma.card [ + prop.ref element + prop.onMouseDown(fun e -> // resize + e.preventDefault() + e.stopPropagation() + let startPosition = {X = int e.clientX; Y = int e.clientY} + let startSize = {X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight} + let onmousemove = ResizeEventListener.onmousemove startPosition startSize setSize + let onmouseup = fun e -> ResizeEventListener.onmouseup (prefix, element) onmousemove + Browser.Dom.document.addEventListener("mousemove", onmousemove) + let config = createEmpty<AddEventListenerOptions> + config.once <- true + Browser.Dom.document.addEventListener("mouseup", onmouseup, config) + ) + prop.style [ + style.zIndex 40 + style.cursor.eastWestResize//style.cursor.northWestSouthEastResize ; + style.display.flex + style.paddingRight(2); + style.overflow.visible + style.position.fixedRelativeToWindow + style.minWidth.minContent + if size.IsSome then + style.width size.Value.X + //style.height size.Value.Y + if position.IsNone then + //style.transform.translate (length.perc -50,length.perc -50) + style.top (length.perc 20); style.left (length.perc 20); + else + style.top position.Value.Y; style.left position.Value.X; + ] + prop.children content + ] + resizeElement <| Html.div [ + prop.onMouseDown(fun e -> e.stopPropagation()) + prop.style [style.cursor.defaultCursor; style.display.flex; style.flexDirection.column; style.flexGrow 1] + prop.children [ + Bulma.cardHeader [ + prop.onMouseDown(fun e -> // move + e.preventDefault() + e.stopPropagation() + let x = e.clientX - element.current.Value.offsetLeft + let y = e.clientY - element.current.Value.offsetTop; + let startPosition = {X = int x; Y = int y} + let onmousemove = MoveEventListener.onmousemove element startPosition setPosition + let onmouseup = fun e -> MoveEventListener.onmouseup (prefix, element) onmousemove + Browser.Dom.document.addEventListener("mousemove", onmousemove) + let config = createEmpty<AddEventListenerOptions> + config.once <- true + Browser.Dom.document.addEventListener("mouseup", onmouseup, config) + ) + prop.style [style.cursor.move] + prop.children [ + Bulma.cardHeaderTitle.p Html.none + Bulma.cardHeaderIcon.a [ + Bulma.delete [ + prop.onClick (fun e -> e.stopPropagation(); rmv e) + ] + ] + ] + ] + Bulma.cardContent [ + prop.style [style.overflow.inheritFromParent] + prop.children [ + content + if help.IsSome then Elements.helpExtendButton (fun _ -> setHelpIsActive (not helpIsActive)) + ] + ] + Bulma.cardFooter [ + prop.style [style.padding 5] + if help.IsSome then + prop.children [ + Bulma.content [ + prop.className "widget-help-container" + prop.style [style.overflow.hidden; if not helpIsActive then style.display.none; ] + prop.children [ + help.Value + ] + ] + ] + ] + ] + ] + + static member BuildingBlock (model, dispatch, rmv: MouseEvent -> unit) = + let content = BuildingBlock.SearchComponent.Main model dispatch + let help = Html.div [ + Html.p "Add a new Building Block." + Html.ul [ + Html.li "If a cell is selected, a new Building Block is added to the right of the selected cell." + Html.li "If no cell is selected, a new Building Block is appended at the right end of the table." + ] + ] + let prefix = BuildingBlockWidgets + Widget.Base(content, prefix, rmv, help) + + + [<ReactComponent>] + static member Templates (model: Messages.Model, dispatch, rmv: MouseEvent -> unit) = + 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 selectContent() = + [ + Protocol.Search.FileSortElement(model, config, setConfig) + Protocol.Search.Component (filteredTemplates, model, dispatch, length.px 350) + ] + let insertContent() = + [ + Bulma.field.div [ + Protocol.TemplateFromDB.addFromDBToTableButton model dispatch + ] + Bulma.field.div [ + prop.style [style.maxHeight (length.px 350); style.overflow.auto] + prop.children [ + Protocol.TemplateFromDB.displaySelectedProtocolEle model dispatch + ] + ] + ] + let content = + let switchContent = if model.ProtocolState.TemplateSelected.IsNone then selectContent() else insertContent() + Html.div [ + prop.children switchContent + ] + + let help = Protocol.Search.InfoField() + let prefix = TemplatesWidgets + Widget.Base(content, prefix, rmv, help) + + static member FilePicker (model, dispatch, rmv) = + let content = Html.div [ + FilePicker.uploadButton model dispatch + if model.FilePickerState.FileNames <> [] then + FilePicker.fileSortElements model dispatch + + Bulma.field.div [ + prop.style [style.maxHeight (length.px 350); style.overflow.auto] + prop.children [ + FilePicker.FileNameTable.table model dispatch + ] + ] + //fileNameElements model dispatch + FilePicker.insertButton model dispatch + ] + let prefix = FilePickerWidgets + let help = Html.div [] + Widget.Base(content, prefix, rmv, help) \ No newline at end of file diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index e20df903..aed5f8a1 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -8,13 +8,12 @@ open Fable.Remoting.Client open Fable.SimpleJson open TermTypes -open TemplateTypes open ExcelColors open OfficeInterop open OfficeInteropTypes open Model open Routing -open ARCtrl.ISA +open ARCtrl open Fable.Core type System.Exception with @@ -43,40 +42,26 @@ module AdvancedSearch = type Msg = | GetSearchResults of {| config:AdvancedSearchTypes.AdvancedSearchOptions; responseSetter: Term [] -> unit |} +module Ontologies = + + type Msg = + | GetOntologies + type DevMsg = | LogTableMetadata | GenericLog of Cmd<Messages.Msg> * (string*string) | GenericInteropLogs of Cmd<Messages.Msg> * InteropLogging.Msg list | GenericError of Cmd<Messages.Msg> * exn | UpdateDisplayLogList of LogItem list - -type ApiRequestMsg = - | GetNewUnitTermSuggestions of string - | FetchAllOntologies - /// TermSearchable [] is created by officeInterop and passed to server for db search. - | SearchForInsertTermsRequest of TermSearchable [] - // - | GetAppVersion - -type ApiResponseMsg = - | UnitTermSuggestionResponse of Term [] - | FetchAllOntologiesResponse of Ontology [] - | SearchForInsertTermsResponse of TermSearchable [] - // - | GetAppVersionResponse of string - -type ApiMsg = - | Request of ApiRequestMsg - | Response of ApiResponseMsg - | ApiError of exn - | ApiSuccess of (string*string) type StyleChangeMsg = | UpdateColorMode of ColorMode -type PersistentStorageMsg = +module PersistentStorage = + type Msg = | NewSearchableOntologies of Ontology [] | UpdateAppVersion of string + | UpdateShowSidebar of bool module FilePicker = type Msg = @@ -88,32 +73,25 @@ module BuildingBlock = open TermSearch type Msg = + | UpdateHeaderWithIO of BuildingBlock.HeaderCellType * IOType | UpdateHeaderCellType of BuildingBlock.HeaderCellType | UpdateHeaderArg of U2<OntologyAnnotation,IOType> option | UpdateBodyCellType of BuildingBlock.BodyCellType | UpdateBodyArg of U2<string, OntologyAnnotation> option - // Below everything is more or less deprecated - // Is still used for unit update in office - | SearchUnitTermTextChange of searchString:string - | UnitTermSuggestionUsed of unitTerm:Term - | NewUnitTermSuggestions of Term [] module Protocol = type Msg = - // // ------ Process from file ------ - | ParseUploadedFileRequest of raw: byte [] - | ParseUploadedFileResponse of (string * InsertBuildingBlock []) [] // Client - | RemoveUploadedFileParsed + | UpdateTemplates of Template [] + | UpdateLoading of bool + | RemoveSelectedProtocol // // ------ Protocol from Database ------ + | GetAllProtocolsForceRequest | GetAllProtocolsRequest - | GetAllProtocolsResponse of string [] - | SelectProtocol of ARCtrl.Template.Template + | GetAllProtocolsResponse of string + | SelectProtocol of Template | ProtocolIncreaseTimesUsed of protocolName:string - // Client - | RemoveSelectedProtocol - | UpdateLoading of bool type BuildingBlockDetailsMsg = | GetSelectedBuildingBlockTermsRequest of TermSearchable [] @@ -133,31 +111,19 @@ type Model = { PageState : PageState ///Data that needs to be persistent once loaded PersistentStorageState : PersistentStorageState - ///Debouncing - DebouncerState : Debouncer.State ///Error handling, Logging, etc. DevState : DevState ///States regarding term search TermSearchState : TermSearch.Model ///Use this in the future to model excel stuff like table data ExcelState : OfficeInterop.Model - /// This should be removed. Overhead making maintainance more difficult - /// "Use this to log Api calls and maybe handle them better" - ApiState : ApiState ///States regarding File picker functionality FilePickerState : FilePicker.Model ProtocolState : Protocol.Model ///Insert annotation columns AddBuildingBlockState : BuildingBlock.Model - ///Create Validation scheme for Table - ValidationState : Validation.Model ///Used to show selected building block information BuildingBlockDetailsState : BuildingBlockDetailsState - ///Used to manage all custom xml settings - SettingsXmlState : SettingsXml.Model - JsonExporterModel : JsonExporter.Model - TemplateMetadataModel : TemplateMetadata.Model - DagModel : Dag.Model CytoscapeModel : Cytoscape.Model /// Contains all information about spreadsheet view SpreadsheetModel : Spreadsheet.Model @@ -165,35 +131,23 @@ type Model = { } with member this.updateByExcelState (s:OfficeInterop.Model) = { this with ExcelState = s} - member this.updateByJsonExporterModel (m:JsonExporter.Model) = - { this with JsonExporterModel = m} - member this.updateByTemplateMetadataModel (m:TemplateMetadata.Model) = - { this with TemplateMetadataModel = m} - member this.updateByDagModel (m:Dag.Model) = - { this with DagModel = m} type Msg = -| Bounce of (System.TimeSpan*string*Msg) -| DebouncerSelfMsg of Debouncer.SelfMessage<Msg> -| Api of ApiMsg | DevMsg of DevMsg +| OntologyMsg of Ontologies.Msg | TermSearchMsg of TermSearch.Msg | AdvancedSearchMsg of AdvancedSearch.Msg | OfficeInteropMsg of OfficeInterop.Msg -| PersistentStorage of PersistentStorageMsg +| PersistentStorageMsg of PersistentStorage.Msg | FilePickerMsg of FilePicker.Msg | BuildingBlockMsg of BuildingBlock.Msg | ProtocolMsg of Protocol.Msg -| JsonExporterMsg of JsonExporter.Msg -| TemplateMetadataMsg of TemplateMetadata.Msg | BuildingBlockDetails of BuildingBlockDetailsMsg | CytoscapeMsg of Cytoscape.Msg | SpreadsheetMsg of Spreadsheet.Msg -| DagMsg of Dag.Msg /// This is used to forward Msg to SpreadsheetMsg/OfficeInterop | InterfaceMsg of SpreadsheetInterface.Msg //| SettingsProtocolMsg of SettingsProtocolMsg -| TopLevelMsg of TopLevelMsg | UpdatePageState of Routing.Route option | UpdateIsExpert of bool | Batch of seq<Messages.Msg> diff --git a/src/Client/Modals/MoveColumn.fs b/src/Client/Modals/MoveColumn.fs new file mode 100644 index 00000000..41c0262f --- /dev/null +++ b/src/Client/Modals/MoveColumn.fs @@ -0,0 +1,115 @@ +namespace Modals + +open Feliz +open Feliz.Bulma +open Model +open Messages +open Shared +open OfficeInteropTypes + +open ARCtrl + +type MoveColumn = + + [<ReactComponent>] + static member InputField(index: int, set, max: int, submit) = + let input, setInput = React.useState(index) + Bulma.field.div [ + prop.className "is-grouped is-justify-content-space-between" + prop.style [style.gap (length.rem 1)] + prop.children [ + Bulma.field.div [ + Bulma.label "Preview" + Bulma.field.div [ + Bulma.field.hasAddons + prop.children [ + Bulma.control.div [ + Bulma.control.isExpanded + prop.children [ + Bulma.input.number [ + prop.onChange(fun i -> setInput i) + prop.defaultValue input + prop.min 0 + prop.max max + ] + ] + ] + Bulma.control.div [ + Bulma.button.button [ + prop.onClick(fun _ -> set (index,input)) + prop.text "Apply" + ] + ] + ] + ] + ] + Bulma.field.div [ + Bulma.label "Update Table" + Bulma.button.a [ + Bulma.color.isInfo + prop.onClick (submit input) + prop.text "Submit" + ] + ] + ] + ] + + [<ReactComponent>] + static member Main (columnIndex: int, model: Messages.Model, dispatch) (rmv: _ -> unit) = + let table = model.SpreadsheetModel.ActiveTable + let state, setState = React.useState(Array.ofSeq table.Headers) + let index, setIndex = React.useState(columnIndex) + let updateIndex(current, next) = + setIndex next + let nextState = ResizeArray(state) + Helper.arrayMoveColumn current next nextState + setState (Array.ofSeq nextState) + let submit = fun i e -> + Spreadsheet.MoveColumn(columnIndex, i) |> SpreadsheetMsg |> dispatch + rmv e + Bulma.modal [ + Bulma.modal.isActive + prop.children [ + Bulma.modalBackground [ prop.onClick rmv ] + Bulma.modalCard [ + prop.style [style.maxHeight(length.percent 70); style.overflowY.hidden] + prop.children [ + Bulma.modalCardHead [ + Bulma.modalCardTitle "Move Column" + Bulma.delete [ prop.onClick rmv ] + ] + Bulma.modalCardBody [ + MoveColumn.InputField(index, updateIndex, state.Length-1, submit) + Bulma.tableContainer [ + prop.style [style.maxHeight 400; style.overflowY.auto] + prop.children [ + Bulma.table [ + Bulma.table.isFullWidth + prop.children [ + Html.thead [ + Html.tr [ + Html.th "Index" + Html.th "Column" + ] + ] + Html.tbody [ + for i in 0 .. state.Length-1 do + Html.tr [ + if i = index then + Bulma.color.hasBackgroundDanger; + prop.className "has-background-danger" + prop.children [ + Html.td i + Html.td (state.[i].ToString()) + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] \ No newline at end of file diff --git a/src/Client/Modals/SelectiveImportModal.fs b/src/Client/Modals/SelectiveImportModal.fs new file mode 100644 index 00000000..40a2d01b --- /dev/null +++ b/src/Client/Modals/SelectiveImportModal.fs @@ -0,0 +1,311 @@ +namespace Modals + +open Feliz +open Feliz.Bulma +open Model +open Messages +open Shared + +open ARCtrl + +[<RequireQualifiedAccess>] +type private ImportTable = { + Index: int + /// If FullImport is true, the table will be imported in full, otherwise it will be appended to active table. + FullImport: bool +} + +type private SelectiveImportModalState = { + ImportType: ARCtrl.TableJoinOptions + ImportMetadata: bool + ImportTables: ImportTable list +} with + static member init() = + { + ImportType = ARCtrl.TableJoinOptions.Headers + ImportMetadata = false + ImportTables = [] + } + +module private Helper = + + let submitWithMetadata (uploadedFile: ArcFiles) (state: SelectiveImportModalState) (dispatch: Messages.Msg -> unit) = + if not state.ImportMetadata then failwith "Metadata must be imported" + let createUpdatedTables (arcTables: ResizeArray<ArcTable>) = + [ + for it in state.ImportTables do + let sourceTable = arcTables.[it.Index] + let appliedTable = ArcTable.init(sourceTable.Name) + appliedTable.Join(sourceTable, joinOptions=state.ImportType) + appliedTable + ] + |> ResizeArray + let arcFile = + match uploadedFile with + | Assay a as arcFile-> + let tables = createUpdatedTables a.Tables + a.Tables <- tables + arcFile + | Study (s,_) as arcFile -> + let tables = createUpdatedTables s.Tables + s.Tables <- tables + arcFile + | Template t as arcFile -> + let table = createUpdatedTables (ResizeArray[t.Table]) + t.Table <- table.[0] + arcFile + | Investigation _ as arcFile -> + arcFile + SpreadsheetInterface.UpdateArcFile arcFile |> InterfaceMsg |> dispatch + + let submitTables (tables: ResizeArray<ArcTable>) (importState: SelectiveImportModalState) (activeTable: ArcTable) (dispatch: Messages.Msg -> unit) = + if importState.ImportTables.Length = 0 then + () + else + let addMsgs = + importState.ImportTables + |> Seq.filter (fun x -> x.FullImport) + |> Seq.map (fun x -> tables.[x.Index]) + |> Seq.map (fun table -> + let nTable = ArcTable.init(table.Name) + nTable.Join(table, joinOptions=importState.ImportType) + nTable + ) + |> Seq.map (fun table -> SpreadsheetInterface.AddTable table |> InterfaceMsg) + let appendMsg = + let tables = importState.ImportTables |> Seq.filter (fun x -> not x.FullImport) |> Seq.map (fun x -> tables.[x.Index]) + /// Everything will be appended against this table, which in the end will be appended to the main table + let tempTable = ArcTable.init("ThisIsAPlaceholder") + for table in tables do + let preparedTemplate = Table.distinctByHeader tempTable table + tempTable.Join(preparedTemplate, joinOptions=importState.ImportType) + let appendTable = Table.distinctByHeader activeTable tempTable + SpreadsheetInterface.JoinTable (appendTable, None, Some importState.ImportType) |> InterfaceMsg + appendMsg |> dispatch + if Seq.length addMsgs = 0 then () else addMsgs |> Seq.iter dispatch + +open Helper + +type SelectiveImportModal = + + static member private ImportTypeRadio(importType: TableJoinOptions, setImportType: TableJoinOptions -> unit) = + let myradio(target: TableJoinOptions, txt: string) = + let isChecked = importType = target + Html.label [ + prop.className "radio is-unselectable" + prop.children [ + Html.input [ + prop.type'.radio + prop.name "importType" + prop.isChecked isChecked + prop.onChange (fun (b:bool) -> if b then setImportType target) + ] + Html.text txt + ] + ] + + Bulma.box [ + Bulma.field.div [ + Bulma.label [ + Html.i [prop.className "fa-solid fa-cog"] + Html.text (" Import Type") + ] + Bulma.control.div [ + prop.className "is-flex is-justify-content-space-between" + prop.children [ + myradio(ARCtrl.TableJoinOptions.Headers, " Column Headers") + myradio(ARCtrl.TableJoinOptions.WithUnit, " ..With Units") + myradio(ARCtrl.TableJoinOptions.WithValues, " ..With Values") + ] + ] + ] + ] + + static member private MetadataImport(isActive: bool, setActive: bool -> unit, disArcFile: ArcFilesDiscriminate) = + let name = string disArcFile + Bulma.box [ + if isActive then color.hasBackgroundInfo + prop.children [ + Bulma.field.div [ + Bulma.label [ + Html.i [prop.className "fa-solid fa-lightbulb"] + Html.textf " %s Metadata" name + ] + Bulma.control.div [ + Html.label [ + prop.className "checkbox is-unselectable" + prop.children [ + Html.input [ + prop.type'.checkbox + prop.onChange (fun (b:bool) -> setActive b) + ] + Html.text " Import" + ] + ] + ] + Html.span [ + color.hasTextWarning + prop.text "Importing metadata will overwrite the current file." + ] + ] + ] + ] + + [<ReactComponent>] + static member private TableImport(index: int, table: ArcTable, state: SelectiveImportModalState, addTableImport: int -> bool -> unit, rmvTableImport: int -> unit) = + let showData, setShowData = React.useState(false) + let name = table.Name + let radioGroup = "radioGroup_" + name + let import = state.ImportTables |> List.tryFind (fun it -> it.Index = index) + let isActive = import.IsSome + let disableAppend = state.ImportMetadata + Bulma.box [ + if isActive then color.hasBackgroundSuccess + prop.children [ + Bulma.field.div [ + Bulma.label [ + Html.i [prop.className "fa-solid fa-table"] + Html.span (" " + name) + Bulma.button.span [ + if showData then button.isActive + button.isSmall + prop.onClick (fun _ -> setShowData (not showData)) + prop.style [style.float'.right; style.cursor.pointer] + prop.children [ + Bulma.icon [ + icon.isSmall + prop.children [ + Html.i [ + prop.style [style.transitionProperty "transform"; style.transitionDuration (System.TimeSpan.FromSeconds 0.35)] + prop.className ["fa-solid"; "fa-angle-down"; if showData then "fa-rotate-180"] + ] + ] + ] + ] + ] + ] + Bulma.control.div [ + Html.label [ + let isInnerActive = isActive && import.Value.FullImport + prop.className "radio is-unselectable" + prop.children [ + Html.input [ + prop.type'.radio + prop.name radioGroup + prop.isChecked isInnerActive + prop.onChange (fun (b:bool) -> addTableImport index true) + ] + Html.text " Import" + ] + ] + Html.label [ + let isInnerActive = isActive && not import.Value.FullImport + prop.className "radio is-unselectable" + prop.children [ + Html.input [ + prop.type'.radio + prop.name radioGroup + if disableAppend then prop.disabled true + prop.isChecked isInnerActive + prop.onChange (fun (b:bool) -> addTableImport index false) + ] + Html.text " Append to active table" + ] + ] + Html.label [ + let isInnerActive = not isActive + prop.className "radio is-unselectable" + prop.children [ + Html.input [ + prop.type'.radio + prop.name radioGroup + prop.isChecked isInnerActive + prop.onChange (fun (b:bool) -> rmvTableImport index) + ] + Html.text " No Import" + ] + ] + ] + ] + if showData then + Bulma.field.div [ + Bulma.tableContainer [ + Bulma.table [ + Bulma.table.isBordered + prop.children [ + Html.thead [ + Html.tr [ + for c in table.Headers do + Html.th (c.ToString()) + ] + ] + Html.tbody [ + for ri in 0 .. (table.RowCount-1) do + let row = table.GetRow(ri, true) + Html.tr [ + for c in row do + Html.td (c.ToString()) + ] + ] + ] + ] + ] + ] + ] + ] + + [<ReactComponent>] + static member Main (import: ArcFiles) (model: Spreadsheet.Model) dispatch (rmv: _ -> unit) = + let state, setState = React.useState(SelectiveImportModalState.init) + let tables, disArcfile = + match import with + | Assay a -> a.Tables, ArcFilesDiscriminate.Assay + | Study (s,_) -> s.Tables, ArcFilesDiscriminate.Study + | Template t -> ResizeArray([t.Table]), ArcFilesDiscriminate.Template + | Investigation _ -> ResizeArray(), ArcFilesDiscriminate.Investigation + let setMetadataImport = fun b -> + {state with ImportMetadata = b; ImportTables = state.ImportTables |> List.map (fun t -> {t with FullImport = true})} |> setState + let addTableImport = fun (i:int) (fullImport: bool) -> + let newImportTable: ImportTable = {Index = i; FullImport = fullImport} + let newImportTables = newImportTable::state.ImportTables |> List.distinct + {state with ImportTables = newImportTables} |> setState + let rmvTableImport = fun i -> + {state with ImportTables = state.ImportTables |> List.filter (fun it -> it.Index <> i)} |> setState + Bulma.modal [ + Bulma.modal.isActive + prop.children [ + Bulma.modalBackground [ prop.onClick rmv ] + Bulma.modalCard [ + prop.style [style.maxHeight(length.percent 70); style.overflowY.hidden] + prop.children [ + Bulma.modalCardHead [ + Bulma.modalCardTitle "Import" + Bulma.delete [ prop.onClick rmv ] + ] + Bulma.modalCardBody [ + prop.className "p-5" + prop.children [ + SelectiveImportModal.ImportTypeRadio(state.ImportType, fun it -> {state with ImportType = it} |> setState) + SelectiveImportModal.MetadataImport(state.ImportMetadata, setMetadataImport, disArcfile) + for ti in 0 .. (tables.Count-1) do + let t = tables.[ti] + SelectiveImportModal.TableImport(ti, t, state, addTableImport, rmvTableImport) + ] + ] + Bulma.modalCardFoot [ + Bulma.button.button [ + color.isInfo + prop.style [style.marginLeft length.auto] + prop.text "Submit" + prop.onClick(fun e -> + match state.ImportMetadata with + | true -> submitWithMetadata import state dispatch + | false -> submitTables tables state model.ActiveTable dispatch + rmv e + ) + ] + ] + ] + ] + ] + ] diff --git a/src/Client/Modals/UpdateColumn.fs b/src/Client/Modals/UpdateColumn.fs new file mode 100644 index 00000000..709be490 --- /dev/null +++ b/src/Client/Modals/UpdateColumn.fs @@ -0,0 +1,257 @@ +namespace Modals + +open Feliz +open Feliz.Bulma +open Model +open Messages +open Shared + +open ARCtrl +open System.Text.RegularExpressions + +[<RequireQualifiedAccess>] +type private FunctionPage = +| Create +| Update + +module private Components = + + open System + + let calculateRegex (regex:string) (input: string) = + try + let regex = Regex(regex) + let m = regex.Match(input) + match m.Success with + | true -> m.Index, m.Length + | false -> 0,0 + with + | _ -> 0,0 + + + let split (start: int) (length: int) (str: string) = + let s0, s1 = + str |> Seq.toList |> List.splitAt (start) + let s1, s2 = + s1 |> Seq.toList |> List.splitAt (length) + String.Join("", s0), String.Join("", s1), String.Join("", s2) + + let Tab(targetPage: FunctionPage, currentPage, setPage) = + Bulma.tab [ + if targetPage = currentPage then tab.isActive + prop.onClick (fun _ -> setPage targetPage) + prop.children [ + Html.a [ + prop.text (targetPage.ToString()) + ] + ] + ] + + let TabNavigation(currentPage, setPage) = + Bulma.tabs [ + prop.style [style.flexGrow 1] + tabs.isCentered + tabs.isFullWidth + prop.children [ + Html.ul [ + Tab(FunctionPage.Create, currentPage, setPage) + Tab(FunctionPage.Update, currentPage, setPage) + ] + ] + ] + + let PreviewRow(index:int,cell0: string, cell: string, markedIndices: int*int) = + Html.tr [ + Html.td index + Html.td [ + let s0,marked,s2 = split (fst markedIndices) (snd markedIndices) cell0 + Html.span s0 + Html.span [ + prop.className "has-background-info" + prop.text marked + ] + Html.span s2 + ] + Html.td (cell) + ] + + let PreviewTable(column: CompositeColumn, cellValues: string [], regex) = + Bulma.field.div [ + Bulma.label "Preview" + Bulma.tableContainer [ + Bulma.table [ + Html.thead [ + Html.tr [Html.th "";Html.th "Before"; Html.th "After"] + ] + Html.tbody [ + let previewCount = 5 + let preview = takeFromArray previewCount cellValues + for i in 0 .. (preview.Length-1) do + let cell0 = column.Cells.[i].ToString() + let cell = preview.[i] + let regexMarkedIndex = calculateRegex regex cell0 + PreviewRow(i,cell0,cell,regexMarkedIndex) + ] + ] + ] + ] + +type UpdateColumn = + + [<ReactComponent>] + static member private CreateForm(cellValues: string [], setPreview) = + let baseStr, setBaseStr = React.useState("") + let suffix, setSuffix = React.useState(false) + let updateCells (baseStr: string) (suffix:bool) = + cellValues + |> Array.mapi (fun i c -> + match suffix with + | true -> baseStr + string (i+1) + | false -> baseStr + ) + |> setPreview + Bulma.field.div [ + Bulma.field.div [ + Bulma.label "Base" + Bulma.input.text [ + prop.autoFocus true + prop.valueOrDefault baseStr + prop.onChange(fun s -> + setBaseStr s + updateCells s suffix + ) + ] + ] + Bulma.field.div [ + Bulma.control.div [ + Html.label [ + prop.className "is-flex is-align-items-center checkbox" + prop.style [style.gap (length.rem 0.5)] + prop.children [ + Html.input [ + prop.type' "checkbox" + prop.isChecked suffix + prop.onChange(fun e -> + setSuffix e + updateCells baseStr e + ) + ] + Bulma.help "Add number suffix" + ] + ] + ] + ] + ] + + [<ReactComponent>] + static member private UpdateForm(cellValues: string [], setPreview, regex: string, setRegex: string -> unit) = + let replacement, setReplacement = React.useState("") + let updateCells (replacement: string) (regex: string) = + if regex <> "" then + try + let regex = Regex(regex) + cellValues + |> Array.mapi (fun i c -> + let m = regex.Match(c) + match m.Success with + | true -> + let replaced = c.Replace(m.Value, replacement) + replaced + | false -> + c + ) + |> setPreview + with + | _ -> () + else + () + Bulma.field.div [ + Bulma.field.div [ + Html.div [ + prop.className "is-flex is-flex-direction-row" + prop.style [style.gap (length.rem 1)] + prop.children [ + Bulma.control.div [ + prop.style [style.flexGrow 1] + prop.children [ + Bulma.label "Regex" + Bulma.input.text [ + prop.autoFocus true + prop.valueOrDefault regex + prop.onChange (fun s -> + setRegex s; + updateCells replacement s + ) + ] + ] + ] + Bulma.control.div [ + prop.style [style.flexGrow 1] + prop.children [ + Bulma.label "Replacement" + Bulma.input.text [ + prop.valueOrDefault replacement + prop.onChange (fun s -> + setReplacement s; + updateCells s regex + ) + ] + ] + ] + ] + ] + ] + ] + + [<ReactComponent>] + static member Main(index: int, column: CompositeColumn, dispatch) (rmv: _ -> unit) = + let getCellStrings() = column.Cells |> Array.map (fun c -> c.ToString()) + let preview, setPreview = React.useState(getCellStrings) + let initPage = if preview.Length = 0 || preview |> String.concat "" = "" then FunctionPage.Create else FunctionPage.Update + let currentPage, setPage = React.useState(initPage) + /// This state is only used for update logic + let regex, setRegex = React.useState("") + let setPage = fun p -> + if p <> FunctionPage.Update then + setRegex "" + setPage p + let submit = fun () -> + preview + |> Array.map (fun x -> CompositeCell.FreeText x) + |> fun x -> CompositeColumn.create(column.Header, x) + |> fun x -> Spreadsheet.SetColumn (index, x) + |> SpreadsheetMsg + |> dispatch + Bulma.modal [ + Bulma.modal.isActive + prop.children [ + Bulma.modalBackground [ prop.onClick rmv ] + Bulma.modalCard [ + prop.style [style.maxHeight(length.percent 70); style.overflowY.hidden] + prop.children [ + Bulma.modalCardHead [ + Bulma.modalCardTitle "Update Column" + Bulma.delete [ prop.onClick rmv ] + ] + Bulma.modalCardBody [ + Components.TabNavigation(currentPage, setPage) + match currentPage with + | FunctionPage.Create -> UpdateColumn.CreateForm(getCellStrings(), setPreview) + | FunctionPage.Update -> UpdateColumn.UpdateForm(getCellStrings(), setPreview, regex, setRegex) + Components.PreviewTable(column, preview, regex) + ] + Bulma.modalCardFoot [ + Bulma.button.button [ + color.isInfo + prop.style [style.marginLeft length.auto] + prop.text "Submit" + prop.onClick(fun e -> + submit() + rmv e + ) + ] + ] + ] + ] + ] + ] diff --git a/src/Client/Model.fs b/src/Client/Model.fs index 12273014..4d8c6327 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -4,7 +4,6 @@ open Fable.React open Fable.React.Props open Shared open TermTypes -open TemplateTypes open Thoth.Elmish open Routing @@ -89,7 +88,7 @@ type LogItem = module TermSearch = - open ARCtrl.ISA + open ARCtrl type Model = { SelectedTerm : OntologyAnnotation option @@ -143,39 +142,17 @@ type PersistentStorageState = { SearchableOntologies : (Set<string>*Ontology) [] AppVersion : string Host : Swatehost option + ShowSideBar : bool HasOntologiesLoaded : bool } with static member init () = { SearchableOntologies = [||] Host = None AppVersion = "" + ShowSideBar = false HasOntologiesLoaded = false } -type ApiCallStatus = - | IsNone - | Pending - | Successfull - | Failed of string - -type ApiCallHistoryItem = { - FunctionName : string - Status : ApiCallStatus -} - -type ApiState = { - currentCall : ApiCallHistoryItem - callHistory : ApiCallHistoryItem list -} with - static member init() = { - currentCall = ApiState.noCall - callHistory = [] - } - static member noCall = { - FunctionName = "None" - Status = IsNone - } - type PageState = { CurrentPage : Routing.Route IsExpert : bool @@ -199,7 +176,7 @@ open Fable.Core module BuildingBlock = - open ARCtrl.ISA + open ARCtrl type [<RequireQualifiedAccess>] HeaderCellType = | Component @@ -280,14 +257,6 @@ module BuildingBlock = BodyCellType : BodyCellType BodyArg : U2<string, OntologyAnnotation> option - // Below everything is more or less deprecated - // This section is used to add a unit directly to an already existing building block - Unit2TermSearchText : string - Unit2SelectedTerm : Term option - Unit2TermSuggestions : Term [] - HasUnit2TermSuggestionsLoading : bool - ShowUnit2TermSuggestions : bool - } with static member init () = { @@ -295,14 +264,6 @@ module BuildingBlock = HeaderArg = None BodyCellType = BodyCellType.Term BodyArg = None - - // Below everything is more or less deprecated - // This section is used to add a unit directly to an already existing building block - Unit2TermSearchText = "" - Unit2SelectedTerm = None - Unit2TermSuggestions = [||] - ShowUnit2TermSuggestions = false - HasUnit2TermSuggestionsLoading = false } member this.TryHeaderOA() = @@ -325,46 +286,47 @@ module BuildingBlock = | Some (U2.Case1 s) -> Some s | _ -> None -/// Validation scheme for Table -module Validation = - type Model = { - ActiveTableBuildingBlocks : BuildingBlock [] - TableValidationScheme : OfficeInterop.CustomXmlTypes.Validation.TableValidation - // Client view related - DisplayedOptionsId : int option - } with - static member init () = { - ActiveTableBuildingBlocks = [||] - TableValidationScheme = OfficeInterop.CustomXmlTypes.Validation.TableValidation.init() - DisplayedOptionsId = None - } - module Protocol = [<RequireQualifiedAccess>] - type CuratedCommunityFilter = - | Both + type CommunityFilter = + | All | OnlyCurated - | OnlyCommunity + | Community of string + + member this.ToStringRdb() = + match this with + | All -> "All" + | OnlyCurated -> "DataPLANT official" + | Community name -> name + + static member fromString(str:string) = + match str with + | "All" -> All + | "DataPLANT official" -> OnlyCurated + | anyElse -> Community anyElse + + static member CommunityFromOrganisation(org: ARCtrl.Organisation) = + match org with + | ARCtrl.Organisation.DataPLANT -> None + | ARCtrl.Organisation.Other name -> Some <| Community name /// This model is used for both protocol insert and protocol search type Model = { // Client Loading : bool - // // ------ Process from file ------ - UploadedFileParsed : (string*InsertBuildingBlock []) [] + LastUpdated : System.DateTime option // ------ Protocol from Database ------ - ProtocolSelected : ARCtrl.Template.Template option - ProtocolsAll : ARCtrl.Template.Template [] + TemplateSelected : ARCtrl.Template option + Templates : ARCtrl.Template [] } with static member init () = { // Client Loading = false - ProtocolSelected = None - // // ------ Process from file ------ - UploadedFileParsed = [||] + LastUpdated = None + TemplateSelected = None // ------ Protocol from Database ------ - ProtocolsAll = [||] + Templates = [||] } type RequestBuildingBlockInfoStates = @@ -386,38 +348,4 @@ type BuildingBlockDetailsState = { BuildingBlockValues = [||] } -module SettingsXml = - type Model = { - // // Client // // - // Validation xml - ActiveSwateValidation : obj option //OfficeInterop.Types.Xml.ValidationTypes.TableValidation option - NextAnnotationTableForActiveValidation : string option - // Protocol group xml - ActiveProtocolGroup : obj option //OfficeInterop.Types.Xml.GroupTypes.ProtocolGroup option - NextAnnotationTableForActiveProtGroup : string option - // Protocol - ActiveProtocol : obj option //OfficeInterop.Types.Xml.GroupTypes.Protocol option - NextAnnotationTableForActiveProtocol : string option - // - RawXml : string option - NextRawXml : string option - FoundTables : string [] - ValidationXmls : obj [] //OfficeInterop.Types.Xml.ValidationTypes.TableValidation [] - } with - static member init () = { - // Client - ActiveSwateValidation = None - NextAnnotationTableForActiveValidation = None - ActiveProtocolGroup = None - NextAnnotationTableForActiveProtGroup = None - ActiveProtocol = None - // Unused - NextAnnotationTableForActiveProtocol = None - // - RawXml = None - NextRawXml = None - FoundTables = [||] - ValidationXmls = [||] - } - // The main MODEL was shifted to 'Messages.fs' to allow saving 'Msg' diff --git a/src/Client/OfficeInterop/Functions/TemplateMetadataFunctions.fs b/src/Client/OfficeInterop/Functions/TemplateMetadataFunctions.fs deleted file mode 100644 index b1f0c431..00000000 --- a/src/Client/OfficeInterop/Functions/TemplateMetadataFunctions.fs +++ /dev/null @@ -1,130 +0,0 @@ -module OfficeInterop.TemplateMetadataFunctions - -open System - -open Fable.Core -open ExcelJS.Fable -open Excel -open GlobalBindings - -open Shared.OfficeInteropTypes -open Shared.TemplateTypes.Metadata - -let private colorOuterBordersWhite (borderSeq:seq<RangeBorder>) = - borderSeq - |> Seq.iter (fun border -> - if border.sideIndex = U2.Case1 BorderIndex.EdgeBottom || border.sideIndex = U2.Case1 BorderIndex.EdgeLeft || border.sideIndex = U2.Case1 BorderIndex.EdgeRight || border.sideIndex = U2.Case1 BorderIndex.EdgeTop then - border.color <- NFDIColors.white - ) - -let private colorTopBottomBordersWhite (borderSeq:seq<RangeBorder>) = - borderSeq - |> Seq.iter (fun border -> - if border.sideIndex = U2.Case1 BorderIndex.EdgeBottom || border.sideIndex = U2.Case1 BorderIndex.EdgeTop then - border.color <- NFDIColors.white - ) - -let rec extendMetadataFields (metadatafields:MetadataField) = - let children = metadatafields.Children |> List.collect extendMetadataFields - if metadatafields.Key <> "" && metadatafields.Children.IsEmpty |> not && metadatafields.List then - let metadatafields' = {metadatafields with ExtendedNameKey = $"#{metadatafields.ExtendedNameKey.ToUpper()} list"} - metadatafields'::children - elif metadatafields.Key <> "" && metadatafields.Children.IsEmpty |> not then - let metadatafields' = {metadatafields with ExtendedNameKey = "#" + metadatafields.ExtendedNameKey.ToUpper()} - metadatafields'::children - elif metadatafields.Key <> "" then - metadatafields::children - else - children - -let createTemplateMetadataWorksheet (metadatafields:MetadataField) = - Excel.run (fun context -> - promise { - - let extended = extendMetadataFields metadatafields |> Array.ofList - - let rowLength = float extended.Length - - let! newWorksheet = context.sync().``then``(fun e-> - context.workbook.worksheets.add TemplateMetadataWorksheetName - ) - - let! firstColumn, fstColumnCells, sndColumn, sndColumnCells = context.sync().``then``(fun e -> - let fst = newWorksheet.getRangeByIndexes(0.,0.,rowLength,1.) - let _ = fst.format.borders.load(propertyNames=U2.Case1 "items") - let fstCells = [| - for i in 0. .. rowLength-1. do - let cell = fst.getCell (i,0.) - let _ = cell.format.borders.load(propertyNames=U2.Case1 "items") - yield cell - |] - let sndCells = [| - for i in 0. .. rowLength-1. do - let cell = fst.getCell (i,1.) - let _ = cell.format.borders.load(propertyNames=U2.Case1 "items") - yield cell - |] - let snd = newWorksheet.getRangeByIndexes(0.,1.,rowLength,1.) - fst, fstCells, snd, sndCells - ) - - let newIdent = System.Guid.NewGuid() - let idValueIndex = extended |> Array.findIndex (fun x -> x.Key = RowKeys.TemplateIdKey ) - let descriptionValueIndex = extended |> Array.findIndex (fun x -> x.Key = RowKeys.DescriptionKey ) - let columnValues = - ResizeArray [| - for i in 0 .. int rowLength - 1 do - yield ResizeArray [|Some <| box (extended.[i].ExtendedNameKey)|] - |] - let! update = context.sync().``then``(fun e -> - firstColumn.values <- columnValues - sndColumnCells.[idValueIndex].values <- ResizeArray [|ResizeArray [| newIdent |> box |> Some|]|] - firstColumn.format.autofitColumns() - firstColumn.format.autofitRows() - firstColumn.format.font.bold <- true - firstColumn.format.font.color <- "whitesmoke" - firstColumn.format.borders.items |> colorOuterBordersWhite - firstColumn.format.borders.items |> Seq.iter (fun border -> if border.sideIndex = U2.Case1 BorderIndex.EdgeRight then border.weight <- U2.Case1 BorderWeight.Thick) - sndColumnCells |> Array.iter (fun cell -> cell.format.borders.items |> colorOuterBordersWhite ) - firstColumn.format.verticalAlignment <- U2.Case1 VerticalAlignment.Top - sndColumn.format.verticalAlignment <- U2.Case1 VerticalAlignment.Top - let sndColStyling = - extended - |> Array.iteri (fun i info -> - if info.Children.IsEmpty then - fstColumnCells.[i].format.fill.color <- ExcelColors.Excel.Primary - sndColumnCells.[i].format.fill.color <- ExcelColors.Excel.Tint40 - else - fstColumnCells.[i].format.fill.color <- ExcelColors.Excel.Shade10 - sndColumnCells.[i].format.borders.items |> colorTopBottomBordersWhite - sndColumnCells.[i].format.fill.color <- ExcelColors.Excel.Shade10 - ) - //sndColumn.format.fill.color <- ExcelColors.Excel.Tint40 - sndColumnCells.[idValueIndex].format.fill.color <- NFDIColors.Red.Base - let newComments = - extended - |> Array.iteri (fun i info -> - if info.Description.IsSome && info.Description.Value <> "" then - let targetCellRange : U2<Range,string> = U2.Case1 fstColumnCells.[i] - let content : U2<CommentRichContent,string> = U2.Case2 info.Description.Value - // WARNING! - // If you use "let comment = ..." outside of this if-else case ONLY the comment with reply will be added - if i = idValueIndex then - let comment = context.workbook.comments.add(targetCellRange, content, contentType = ContentType.Plain) - let reply : U2<CommentRichContent,string> = U2.Case2 $"id={newIdent.ToString()}" - let _ = comment.replies.add(reply, contentType = ContentType.Plain) - () - else - let comment = context.workbook.comments.add(targetCellRange, content, contentType = ContentType.Plain) - () - else - () - ) - sndColumn.format.columnWidth <- 300. - sndColumnCells.[descriptionValueIndex].format.rowHeight <- 50. - sndColumn.format.wrapText <- true - newWorksheet.activate() - ) - return "Info", "Created new template metadata sheet!" - } - ) \ No newline at end of file diff --git a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs index 2b35fe26..2f47098f 100644 --- a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs +++ b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs @@ -38,301 +38,53 @@ let update (addBuildingBlockMsg:BuildingBlock.Msg) (state: BuildingBlock.Model) BodyArg = None } nextState, Cmd.none - | UpdateBodyCellType next -> - let nextState = { state with BodyCellType = next } - nextState, Cmd.none - - | SearchUnitTermTextChange (newTerm) -> - - let triggerNewSearch = - newTerm.Length > 2 - - let (delay, bounceId, msgToBounce) = - (System.TimeSpan.FromSeconds 0.5), - "GetNewUnitTermSuggestions", - ( - if triggerNewSearch then - (newTerm) |> (GetNewUnitTermSuggestions >> Request >> Api) - else - DoNothing - ) - - let nextState = { - state with - Unit2TermSearchText = newTerm - Unit2SelectedTerm = None - ShowUnit2TermSuggestions = triggerNewSearch - HasUnit2TermSuggestionsLoading = true - } - - nextState, ((delay, bounceId, msgToBounce) |> Bounce |> Cmd.ofMsg) - - | NewUnitTermSuggestions suggestions -> - + | UpdateHeaderWithIO (hct, iotype) -> let nextState = { - state with - Unit2TermSuggestions = suggestions - ShowUnit2TermSuggestions = true - HasUnit2TermSuggestionsLoading = false - } - - nextState,Cmd.none - - | UnitTermSuggestionUsed suggestion -> - let nextState ={ - state with - Unit2TermSearchText = suggestion.Name - Unit2SelectedTerm = Some suggestion - ShowUnit2TermSuggestions = false - HasUnit2TermSuggestionsLoading = false + state with + HeaderCellType = hct + HeaderArg = Some (Fable.Core.U2.Case2 iotype) + BodyArg = None + BodyCellType = BuildingBlock.BodyCellType.Text } nextState, Cmd.none - -//let addBuildingBlockFooterComponent (model:Model) (dispatch:Messages.Msg -> unit) = -// Content.content [ ] [ -// Label.label [Label.Props [Style [Color model.SiteStyleState.ColorMode.Accent]]] [ -// str (sprintf "More about %s:" (model.AddBuildingBlockState.CurrentBuildingBlock.Type.toString )) -// ] -// Text.p [Props [Style [TextAlign TextAlignOptions.Justify]]] [ -// span [] [model.AddBuildingBlockState.CurrentBuildingBlock.Type.toLongExplanation |> str] -// span [] [str " You can find more information on our "] -// a [Href Shared.URLs.AnnotationPrinciplesUrl; Target "_blank"] [str "website"] -// span [] [str "."] -// ] -// ] + | UpdateBodyCellType next -> + let nextState = { state with BodyCellType = next } + nextState, Cmd.none open SidebarComponents +open Feliz +open Feliz.Bulma -//let addBuildingBlockElements (model:Model) (dispatch:Messages.Msg -> unit) = -// let autocompleteParamsTerm = AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockState model.AddBuildingBlockState -// let autocompleteParamsUnit = AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockUnitState model.AddBuildingBlockState - +//let addUnitToExistingBlockElements (model:Model) (dispatch:Messages.Msg -> unit) = // mainFunctionContainer [ -// AdvancedSearch.advancedSearchModal model autocompleteParamsTerm.ModalId autocompleteParamsTerm.InputId dispatch autocompleteParamsTerm.OnAdvancedSearch -// AdvancedSearch.advancedSearchModal model autocompleteParamsUnit.ModalId autocompleteParamsUnit.InputId dispatch autocompleteParamsUnit.OnAdvancedSearch -// Field.div [] [ -// let autocompleteParams = (AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockState model.AddBuildingBlockState) -// Field.div [Field.HasAddons] [ -// Choose building block type dropdown element -// Control.p [] [ -// Dropdown.dropdown [ -// Dropdown.IsActive model.AddBuildingBlockState.ShowBuildingBlockSelection -// ] [ -// Dropdown.trigger [] [ -// Button.a [Button.OnClick (fun e -> e.stopPropagation(); ToggleSelectionDropdown |> BuildingBlockMsg |> dispatch)] [ -// span [Style [MarginRight "5px"]] [str model.AddBuildingBlockState.CurrentBuildingBlock.Type.toString] -// Fa.i [Fa.Solid.AngleDown] [] -// ] -// ] -// Dropdown.menu [ ] [ -// match model.AddBuildingBlockState.DropdownPage with -// | Model.BuildingBlock.DropdownPage.Main -> -// Helper.DropdownElements.dropdownContentMain model dispatch -// | Model.BuildingBlock.DropdownPage.ProtocolTypes -> -// Helper.DropdownElements.dropdownContentProtocolTypeColumns model dispatch -// | Model.BuildingBlock.DropdownPage.Output -> -// Helper.DropdownElements.dropdownContentOutputColumns model dispatch -// |> fun content -> Dropdown.content [Props [Style [yield! colorControlInArray model.SiteStyleState.ColorMode]] ] content -// ] -// ] -// ] -// Ontology Term search field -// if model.AddBuildingBlockState.CurrentBuildingBlock.Type.isTermColumn && model.AddBuildingBlockState.CurrentBuildingBlock.Type.isFeaturedColumn |> not then -// AutocompleteSearch.autocompleteTermSearchComponentInputComponent -// dispatch -// false // isDisabled -// "Start typing to search" -// None // No input size specified -// autocompleteParams - -// ] -// Ontology Term search preview -// AutocompleteSearch.autocompleteDropdownComponent -// dispatch -// model.SiteStyleState.ColorMode -// autocompleteParams.DropDownIsVisible -// autocompleteParams.DropDownIsLoading -// (AutocompleteSearch.createAutocompleteSuggestions dispatch autocompleteParams model) -// ] -// Ontology Unit Term search field -// if model.AddBuildingBlockState.CurrentBuildingBlock.Type.isTermColumn && model.AddBuildingBlockState.CurrentBuildingBlock.Type.isFeaturedColumn |> not then -// let unitAutoCompleteParams = AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockUnitState model.AddBuildingBlockState -// Field.div [] [ -// Field.div [Field.HasAddons] [ -// Control.p [] [ -// Button.a [ -// Button.Props [Style [ -// if model.AddBuildingBlockState.BuildingBlockHasUnit then Color NFDIColors.Mint.Base else Color NFDIColors.Red.Base -// ]] -// Button.OnClick (fun _ -> -// let inputId = (AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockUnitState model.AddBuildingBlockState).InputId -// if model.AddBuildingBlockState.BuildingBlockHasUnit = true then -// let e = Browser.Dom.document.getElementById inputId -// e?value <- null -// ToggleBuildingBlockHasUnit |> BuildingBlockMsg |> dispatch -// ) -// ] [ -// Fa.i [ -// Fa.Size Fa.FaLarge; -// Fa.Props [Style [AlignSelf AlignSelfOptions.Center; Transform "translateY(1px)"]] -// if model.AddBuildingBlockState.BuildingBlockHasUnit then -// Fa.Solid.Check -// else -// Fa.Solid.Ban -// ] [ ] -// ] -// ] -// Control.p [] [ -// Button.button [Button.IsStatic true; Button.Props [Style [BackgroundColor ExcelColors.Colorfull.white]]] [ -// str (sprintf "This %s has a unit:" (model.AddBuildingBlockState.CurrentBuildingBlock.Type.toString)) -// ] -// ] -// AutocompleteSearch.autocompleteTermSearchComponentInputComponent -// dispatch -// if BuildingBlockHasUnit = false then disabled = true -// (model.AddBuildingBlockState.BuildingBlockHasUnit |> not) -// "Start typing to search" -// None // No input size specified -// unitAutoCompleteParams -// ] -// Ontology Unit Term search preview -// AutocompleteSearch.autocompleteDropdownComponent -// dispatch -// model.SiteStyleState.ColorMode -// unitAutoCompleteParams.DropDownIsVisible -// unitAutoCompleteParams.DropDownIsLoading -// (AutocompleteSearch.createAutocompleteSuggestions dispatch unitAutoCompleteParams model) -// ] - -// div [] [ -// Help.help [Help.Props [Style [Display DisplayOptions.Inline]]] [ -// a [OnClick (fun _ -> AdvancedSearch.ToggleModal (AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockState model.AddBuildingBlockState).ModalId |> AdvancedSearchMsg |> dispatch)] [ -// str "Use advanced search building block" -// ] -// ] -// if model.AddBuildingBlockState.CurrentBuildingBlock.Type.isTermColumn then -// Help.help [Help.Props [Style [Display DisplayOptions.Inline; Float FloatOptions.Right]]] [ -// a [OnClick (fun _ -> AdvancedSearch.ToggleModal (AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockUnitState model.AddBuildingBlockState).ModalId |> AdvancedSearchMsg |> dispatch)] [ -// str "Use advanced search unit" -// ] -// ] -// ] - -// Field.div [] [ -// Button.button [ -// let isValid = model.AddBuildingBlockState.CurrentBuildingBlock |> Helper.isValidBuildingBlock +// Bulma.field.div [ +// Bulma.button.button [ + +// let isValid = model.AddBuildingBlockState.Unit2TermSearchText <> "" +// Bulma.color.isSuccess // if isValid then -// Button.Color Color.IsSuccess -// Button.IsActive true +// Bulma.button.isActive // else -// Button.Color Color.IsDanger -// Button.Props [Disabled true] -// Button.IsFullWidth -// Button.OnClick (fun e -> -// let colName = model.AddBuildingBlockState.CurrentBuildingBlock -// let colTerm = -// if colName.isFeaturedColumn then -// TermMinimal.create colName.Type.toString colName.Type.getFeaturedColumnAccession |> Some -// elif model.AddBuildingBlockState.BuildingBlockSelectedTerm.IsSome && not colName.isSingleColumn then -// TermMinimal.ofTerm model.AddBuildingBlockState.BuildingBlockSelectedTerm.Value |> Some -// else -// None -// let unitTerm = if model.AddBuildingBlockState.UnitSelectedTerm.IsSome && colName.isTermColumn && not colName.isFeaturedColumn then TermMinimal.ofTerm model.AddBuildingBlockState.UnitSelectedTerm.Value |> Some else None -// let newBuildingBlock = InsertBuildingBlock.create colName colTerm unitTerm Array.empty -// SpreadsheetInterface.AddAnnotationBlock newBuildingBlock |> InterfaceMsg |> dispatch +// Bulma.color.isDanger +// prop.disabled true +// Bulma.button.isFullWidth +// prop.onClick (fun _ -> +// let unitTerm = +// if model.AddBuildingBlockState.Unit2SelectedTerm.IsSome then Some <| TermMinimal.ofTerm model.AddBuildingBlockState.Unit2SelectedTerm.Value else None +// match model.AddBuildingBlockState.Unit2TermSearchText with +// | "" -> +// curry GenericLog Cmd.none ("Error", "Cannot execute function with empty unit input") |> DevMsg |> dispatch +// | hasUnitTerm when model.AddBuildingBlockState.Unit2SelectedTerm.IsSome -> +// OfficeInterop.UpdateUnitForCells unitTerm.Value |> OfficeInteropMsg |> dispatch +// | freeText -> +// OfficeInterop.UpdateUnitForCells (TermMinimal.create model.AddBuildingBlockState.Unit2TermSearchText "") |> OfficeInteropMsg |> dispatch // ) -// ] [ -// str "Add building block" +// prop.text "Update unit for cells" // ] // ] // ] -open Feliz -open Feliz.Bulma - -let addUnitToExistingBlockElements (model:Model) (dispatch:Messages.Msg -> unit) = - // /// advanced unit term search 2 - //let autocompleteParamsUnit2 = AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockUnit2State model.AddBuildingBlockState - mainFunctionContainer [ - // advanced unit term search 2 - //AdvancedSearch.advancedSearchModal model autocompleteParamsUnit2.ModalId autocompleteParamsUnit2.InputId dispatch autocompleteParamsUnit2.OnAdvancedSearch - //Bulma.field.div [ - // Bulma.help [ - // b [] [str "Adds a unit to a complete building block." ] - // str " If the building block already has a unit assigned, the new unit is only applied to selected rows of the selected column." - // ] - //] - //Bulma.field.div [ - // let changeUnitAutoCompleteParams = AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockUnit2State model.AddBuildingBlockState - // Bulma.field.div [ - // Bulma.field.hasAddons - // prop.children [ - // Bulma.control.p [ - // Bulma.button.button [ - // Bulma.button.isStatic - // Bulma.color.hasBackgroundWhite - // prop.text "Add unit" - // ] - // ] - // // Add/Update unit ontology term search field - // AutocompleteSearch.autocompleteTermSearchComponentInputComponent - // dispatch - // false // isDisabled - // "Start typing to search" - // None // No input size specified - // changeUnitAutoCompleteParams - // ] - // ] - // // Add/Update Ontology Unit Term search preview - // AutocompleteSearch.autocompleteDropdownComponent - // dispatch - // changeUnitAutoCompleteParams.DropDownIsVisible - // changeUnitAutoCompleteParams.DropDownIsLoading - // (AutocompleteSearch.createAutocompleteSuggestions dispatch changeUnitAutoCompleteParams model) - - //] - //Bulma.help [ - // prop.style [style.display.inlineElement] - // prop.children [ - // Html.a [ - // prop.onClick(fun e -> - // e.preventDefault() - // AdvancedSearch.ToggleModal ( - // AutocompleteSearch.AutocompleteParameters<Term>.ofAddBuildingBlockUnit2State model.AddBuildingBlockState).ModalId - // |> AdvancedSearchMsg - // |> dispatch - // ) - // prop.text "Use advanced search" - // ] - // ] - //] - Bulma.field.div [ - Bulma.button.button [ - - let isValid = model.AddBuildingBlockState.Unit2TermSearchText <> "" - Bulma.color.isSuccess - if isValid then - Bulma.button.isActive - else - Bulma.color.isDanger - prop.disabled true - Bulma.button.isFullWidth - prop.onClick (fun _ -> - let unitTerm = - if model.AddBuildingBlockState.Unit2SelectedTerm.IsSome then Some <| TermMinimal.ofTerm model.AddBuildingBlockState.Unit2SelectedTerm.Value else None - match model.AddBuildingBlockState.Unit2TermSearchText with - | "" -> - curry GenericLog Cmd.none ("Error", "Cannot execute function with empty unit input") |> DevMsg |> dispatch - | hasUnitTerm when model.AddBuildingBlockState.Unit2SelectedTerm.IsSome -> - OfficeInterop.UpdateUnitForCells unitTerm.Value |> OfficeInteropMsg |> dispatch - | freeText -> - OfficeInterop.UpdateUnitForCells (TermMinimal.create model.AddBuildingBlockState.Unit2TermSearchText "") |> OfficeInteropMsg |> dispatch - ) - prop.text "Update unit for cells" - ] - ] - ] let addBuildingBlockComponent (model:Model) (dispatch:Messages.Msg -> unit) = div [ @@ -344,17 +96,14 @@ let addBuildingBlockComponent (model:Model) (dispatch:Messages.Msg -> unit) = // Input forms, etc related to add building block. Bulma.label "Add annotation building blocks (columns) to the annotation table." - //match model.PersistentStorageState.Host with - //| Swatehost.Excel _ -> - // addBuildingBlockElements model dispatch - //| _ -> - // () - SearchComponent.Main model dispatch + mainFunctionContainer [ + SearchComponent.Main model dispatch + ] - match model.PersistentStorageState.Host with - | Some Swatehost.Excel -> - Bulma.label "Add/Update unit reference to existing building block." - // Input forms, etc related to add unit to existing building block. - addUnitToExistingBlockElements model dispatch - | _ -> Html.none + //match model.PersistentStorageState.Host with + //| Some Swatehost.Excel -> + // Bulma.label "Add/Update unit reference to existing building block." + // // Input forms, etc related to add unit to existing building block. + // addUnitToExistingBlockElements model dispatch + //| _ -> Html.none ] \ No newline at end of file diff --git a/src/Client/Pages/BuildingBlock/Dropdown.fs b/src/Client/Pages/BuildingBlock/Dropdown.fs index f2bd479c..7810d325 100644 --- a/src/Client/Pages/BuildingBlock/Dropdown.fs +++ b/src/Client/Pages/BuildingBlock/Dropdown.fs @@ -11,7 +11,7 @@ open Model.BuildingBlock open Model.TermSearch open Model open Messages -open ARCtrl.ISA +open ARCtrl open BuildingBlock.Helper open Fable.Core @@ -97,23 +97,21 @@ module private DropdownElements = let createIOTypeDropdownItem (model: Model) dispatch setUiState (headerType: BuildingBlock.HeaderCellType) (iotype: IOType) = let setIO (ioType) = - Helper.selectHeaderCellType headerType setUiState dispatch - U2.Case2 ioType |> Some |> BuildingBlock.UpdateHeaderArg |> BuildingBlockMsg |> dispatch + { DropdownPage = DropdownPage.Main; DropdownIsActive = false } |> setUiState + (headerType,ioType) |> BuildingBlock.UpdateHeaderWithIO |> BuildingBlockMsg |> dispatch Bulma.dropdownItem.a [ - prop.children [ - match iotype with - | IOType.FreeText s -> - let onSubmit = fun (v: string) -> - let header = IOType.FreeText v - setIO header - FreeTextInputElement onSubmit - | _ -> - Html.div [ - prop.onClick (fun e -> e.stopPropagation(); setIO iotype) - prop.onKeyDown(fun k -> if (int k.which) = 13 then setIO iotype) - prop.text (iotype.ToString()) - ] - ] + match iotype with + | IOType.FreeText s -> + let onSubmit = fun (v: string) -> + let header = IOType.FreeText v + setIO header + prop.children [FreeTextInputElement onSubmit] + | _ -> + prop.onClick (fun e -> e.stopPropagation(); setIO iotype) + prop.onKeyDown(fun k -> if (int k.which) = 13 then setIO iotype) + prop.children [ + Html.div [prop.text (iotype.ToString())] + ] ] /// Main column types subpage for dropdown @@ -151,24 +149,10 @@ module private DropdownElements = /// Output columns subpage for dropdown let dropdownContentIOTypeColumns header state setState (model:Model) dispatch = [ - // Heading - //Bulma.dropdownItem.div [ - // prop.style [style.textAlign.center] - // prop.children [ - // Html.h6 [ - // prop.className "subtitle" - // prop.style [style.fontWeight.bold] - // prop.text name - // ] - // ] - //] - //Bulma.dropdownDivider [] IOType.Source |> createIOTypeDropdownItem model dispatch setState header IOType.Sample |> createIOTypeDropdownItem model dispatch setState header IOType.Material |> createIOTypeDropdownItem model dispatch setState header - IOType.RawDataFile |> createIOTypeDropdownItem model dispatch setState header - IOType.DerivedDataFile |> createIOTypeDropdownItem model dispatch setState header - IOType.ImageFile |> createIOTypeDropdownItem model dispatch setState header + IOType.Data |> createIOTypeDropdownItem model dispatch setState header IOType.FreeText "" |> createIOTypeDropdownItem model dispatch setState header // Navigation element back to main page backToMainDropdownButton setState diff --git a/src/Client/Pages/BuildingBlock/Helper.fs b/src/Client/Pages/BuildingBlock/Helper.fs index b23d4f83..b56c058d 100644 --- a/src/Client/Pages/BuildingBlock/Helper.fs +++ b/src/Client/Pages/BuildingBlock/Helper.fs @@ -4,7 +4,7 @@ open Shared open OfficeInteropTypes open Model open Messages -open ARCtrl.ISA +open ARCtrl open Model.BuildingBlock let isSameMajorHeaderCellType (hct1: BuildingBlock.HeaderCellType) (hct2: BuildingBlock.HeaderCellType) = @@ -18,7 +18,7 @@ let selectHeaderCellType (hct: BuildingBlock.HeaderCellType) setUiState dispatch open Fable.Core let createCompositeHeaderFromState (state: BuildingBlock.Model) = - let getOA() = state.TryHeaderOA() |> Option.defaultValue OntologyAnnotation.empty + let getOA() = state.TryHeaderOA() |> Option.defaultValue (OntologyAnnotation.empty()) let getIOType() = state.TryHeaderIO() |> Option.defaultValue (IOType.FreeText "") match state.HeaderCellType with | HeaderCellType.Component -> CompositeHeader.Component <| getOA() diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index d4f47d3f..7e1a193e 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -1,4 +1,4 @@ -module BuildingBlock.SearchComponent +module BuildingBlock.SearchComponent open Feliz open Feliz.Bulma @@ -11,7 +11,7 @@ open Model.BuildingBlock open Model.TermSearch open Model open Messages -open ARCtrl.ISA +open ARCtrl open BuildingBlock.Helper let private termOrUnitizedSwitch (model:Messages.Model) dispatch = @@ -38,17 +38,15 @@ let private termOrUnitizedSwitch (model:Messages.Model) dispatch = ] ] - open Fable.Core -let private SearchBuildingBlockBodyElement (model: Messages.Model) dispatch = - let id = "SearchBuildingBlockBodyElementID" +[<ReactComponent>] +let private SearchBuildingBlockBodyElement (model: Messages.Model, dispatch) = let element = React.useElementRef() - React.useEffectOnce(fun _ -> element.current <- Some <| Browser.Dom.document.getElementById(id)) - let width = element.current |> Option.map (fun ele -> length.px ele.clientWidth) + Bulma.field.div [ - prop.id id - prop.style [ style.display.flex; style.justifyContent.spaceBetween ] + prop.ref element + prop.style [ style.display.flex; style.justifyContent.spaceBetween; style.position.relative ] prop.children [ termOrUnitizedSwitch model dispatch let setter (oaOpt: OntologyAnnotation option) = @@ -56,18 +54,16 @@ let private SearchBuildingBlockBodyElement (model: Messages.Model) dispatch = BuildingBlock.UpdateBodyArg case |> BuildingBlockMsg |> dispatch let parent = model.AddBuildingBlockState.TryHeaderOA() let input = model.AddBuildingBlockState.TryBodyOA() - Components.TermSearch.Input(setter, dispatch, fullwidth=true, ?input=input, ?parent'=parent, displayParent=false, ?dropdownWidth=width, alignRight=true) + Components.TermSearch.Input(setter, fullwidth=true, ?input=input, ?parent=parent, displayParent=false, ?portalTermSelectArea=element.current, debounceSetter=1000) ] ] -let private SearchBuildingBlockHeaderElement (ui: BuildingBlockUIState) setUi (model: Model) dispatch = +[<ReactComponent>] +let private SearchBuildingBlockHeaderElement (ui: BuildingBlockUIState, setUi, model: Model, dispatch) = let state = model.AddBuildingBlockState - let id = "SearchBuildingBlockHeaderElementID" let element = React.useElementRef() - React.useEffectOnce(fun _ -> element.current <- Some <| Browser.Dom.document.getElementById(id)) - let width = element.current |> Option.map (fun ele -> length.px ele.clientWidth) Bulma.field.div [ - prop.id id + prop.ref element Bulma.field.hasAddons prop.style [style.position.relative] // Choose building block type dropdown element @@ -81,7 +77,7 @@ let private SearchBuildingBlockHeaderElement (ui: BuildingBlockUIState) setUi (m BuildingBlock.UpdateHeaderArg case |> BuildingBlockMsg |> dispatch //selectHeader ui setUi h |> dispatch let input = model.AddBuildingBlockState.TryHeaderOA() - Components.TermSearch.Input(setter, dispatch, ?input=input, isExpanded=true, fullwidth=true, ?dropdownWidth=width, alignRight=true) + Components.TermSearch.Input(setter, fullwidth=true, ?input=input, isExpanded=true, ?portalTermSelectArea=element.current, debounceSetter=1000) elif state.HeaderCellType.HasIOType() then Bulma.control.div [ Bulma.control.isExpanded @@ -102,6 +98,28 @@ let private SearchBuildingBlockHeaderElement (ui: BuildingBlockUIState) setUi (m ] ] +let private scrollIntoViewRetry (id: string) = + let rec loop (iteration: int) = + let headerelement = Browser.Dom.document.getElementById(id) + if isNull headerelement then + if iteration < 5 then + Fable.Core.JS.setTimeout (fun _ -> loop (iteration+1)) 100 |> ignore + else + () + else + let rect = headerelement.getBoundingClientRect() + if rect.left >= 0 && ((rect.right <= Browser.Dom.window.innerWidth) || (rect.right <= Browser.Dom.document.documentElement.clientWidth)) then + () + else + let config = createEmpty<Browser.Types.ScrollIntoViewOptions> + config.behavior <- Browser.Types.ScrollBehavior.Smooth + config.block <- Browser.Types.ScrollAlignment.End + config.``inline`` <- Browser.Types.ScrollAlignment.End + //log headerelement + headerelement.scrollIntoView(config) + loop 0 + + let private addBuildingBlockButton (model: Model) dispatch = let state = model.AddBuildingBlockState Bulma.field.div [ @@ -111,14 +129,21 @@ let private addBuildingBlockButton (model: Model) dispatch = let isValid = Helper.isValidColumn header if isValid then Bulma.color.isSuccess - Bulma.button.isActive else Bulma.color.isDanger prop.disabled true Bulma.button.isFullWidth prop.onClick (fun _ -> - let column = CompositeColumn.create(header, [|if body.IsSome then body.Value|]) + let bodyCells = + if body.IsSome then // create as many body cells as there are rows in the active table + Array.init (model.SpreadsheetModel.ActiveTable.RowCount) (fun _ -> body.Value) + else + Array.empty + let column = CompositeColumn.create(header, bodyCells) + let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel SpreadsheetInterface.AddAnnotationBlock column |> InterfaceMsg |> dispatch + let id = $"Header_{index}_Main" + scrollIntoViewRetry id ) prop.text "Add Column" ] @@ -129,11 +154,9 @@ let Main (model: Model) dispatch = let state_bb, setState_bb = React.useState(BuildingBlockUIState.init) //let state_searchHeader, setState_searchHeader = React.useState(TermSearchUIState.init) //let state_searchBody, setState_searchBody = React.useState(TermSearchUIState.init) - mainFunctionContainer [ - SearchBuildingBlockHeaderElement state_bb setState_bb model dispatch + Html.div [ + SearchBuildingBlockHeaderElement (state_bb, setState_bb, model, dispatch) if model.AddBuildingBlockState.HeaderCellType.IsTermColumn() then - SearchBuildingBlockBodyElement model dispatch - //AdvancedSearch.modal_container state_bb setState_bb model dispatch - //AdvancedSearch.links_container model.AddBuildingBlockState.Header dispatch + SearchBuildingBlockBodyElement (model, dispatch) addBuildingBlockButton model dispatch ] diff --git a/src/Client/Pages/Dag/Dag.fs b/src/Client/Pages/Dag/Dag.fs deleted file mode 100644 index 9ec7e525..00000000 --- a/src/Client/Pages/Dag/Dag.fs +++ /dev/null @@ -1,101 +0,0 @@ -[<RequireQualifiedAccess>] -module Dag.Core - -open Fable.React -open Fable.React.Props -open Fable.Core.JsInterop -open Elmish - -open Shared - -open ExcelColors -open Model -open Messages - -open Dag - -let update (msg:Msg) (currentModel: Messages.Model) : Messages.Model * Cmd<Messages.Msg> = - match msg with - | UpdateLoading loading -> - let nextModel = { - currentModel.DagModel with - Loading = loading - } - currentModel.updateByDagModel nextModel, Cmd.none - | ParseTablesOfficeInteropRequest -> - let nextModel = { - currentModel.DagModel with - Loading = true - } - let cmd = - Cmd.OfPromise.either - OfficeInterop.Core.getBuildingBlocksAndSheets - () - (ParseTablesDagServerRequest >> DagMsg) - (curry GenericError (Dag.UpdateLoading false |> DagMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByDagModel nextModel, cmd - | ParseTablesDagServerRequest (worksheetBuildingBlocksTuple) -> - let cmd = - Cmd.OfAsync.either - Api.dagApi.parseAnnotationTablesToDagHtml - worksheetBuildingBlocksTuple - (ParseTablesDagServerResponse >> DagMsg) - (curry GenericError (Dag.UpdateLoading false |> DagMsg |> Cmd.ofMsg) >> DevMsg) - currentModel, cmd - // - | ParseTablesDagServerResponse dagHtml -> - let nextModel = { - currentModel.DagModel with - Loading = false - DagHtml = Some dagHtml - } - currentModel.updateByDagModel nextModel, Cmd.none - -open Messages -open Feliz -open Feliz.Bulma - -let defaultMessageEle (model:Model) dispatch = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.help [ - str "A " - b [] [str "D"] - str "irected " - b [] [str "A"] - str "cyclic " - b [] [str "G"] - str "raph represents the chain of applied protocols to samples. Within are all intermediate products as well as protocols displayed." - ] - Bulma.help [ - str "This only works if your input and output columns have values." - ] - ] - - Bulma.field.div [ - Bulma.button.a [ - Bulma.button.isFullWidth - Bulma.color.isInfo - prop.onClick(fun _ -> SpreadsheetInterface.ParseTablesToDag |> InterfaceMsg |> dispatch) - prop.text "Display dag" - ] - ] - - if model.DagModel.DagHtml.IsSome then - Bulma.field.div [ - iframe [SrcDoc model.DagModel.DagHtml.Value; Style [Width "100%"; Height "400px"] ] [] - ] - ] - -let mainElement (model:Messages.Model) dispatch = - Bulma.content [ - prop.onSubmit (fun e -> e.preventDefault()) - prop.onKeyDown (fun k -> if (int k.which) = 13 then k.preventDefault()) - prop.children [ - pageHeader "Visualize Protocol Flow" - - Bulma.label "Display directed acyclic graph" - - defaultMessageEle model dispatch - ] - ] \ No newline at end of file diff --git a/src/Client/Pages/FilePicker/FilePickerView.fs b/src/Client/Pages/FilePicker/FilePickerView.fs index e5b69535..9253afeb 100644 --- a/src/Client/Pages/FilePicker/FilePickerView.fs +++ b/src/Client/Pages/FilePicker/FilePickerView.fs @@ -30,42 +30,8 @@ let update (filePickerMsg:FilePicker.Msg) (currentState: FilePicker.Model) : Fil } nextState, Cmd.none -/// This logic only works as soon as we can access electron. Will not work in Browser. -module PathRerooting = - - open Fable.Core - open Fable.Core.JsInterop - - let private normalizePath (path:string) = - path.Replace('\\','/') - - let listOfSupportedDirectories = ["studies"; "assays"; "workflows"; "runs"] - - let private matchesSupportedDirectory (str:string) = - listOfSupportedDirectories |> List.contains str - - /// <summary>Normalizes path and searches for 'listOfSupportedDirectories' (["studies"; "assays"; "workflows"; "runs"]) in path. reroots path to parent of supported directory if found - /// else returns only file name.</summary> - let rerootPath (path:string) = - let sep = '/' - let path = normalizePath path // shadow path variable to normalized - let splitPath = path.Split(sep) - let tryFindLevel = Array.tryFindIndexBack (fun x -> matchesSupportedDirectory x) splitPath - match tryFindLevel with - // if we cannot find any of `listOfSupportedDirectories` we just return the file name - | None -> - splitPath |> Array.last - | Some levelIndex -> - // If we find one of `listOfSupportedDirectories` we want to reroot relative to the folder containing the investigation file. - // It is located one level higher than any of `listOfSupportedDirectories` - let rootPath = - Array.take levelIndex splitPath // one level higher so `levelIndex` instead of `(levelIndex + 1)` - |> String.concat (string sep) - let relativePath = - path.Replace(rootPath + string sep, "") - relativePath - -let uploadButton (model:Messages.Model) dispatch (inputId: string) = +let uploadButton (model:Messages.Model) dispatch = + let inputId = "filePicker_OnFilePickerMainFunc" Bulma.field.div [ Html.input [ prop.style [style.display.none] @@ -87,15 +53,40 @@ let uploadButton (model:Messages.Model) dispatch (inputId: string) = //picker?value <- null ) ] - Bulma.button.button [ - Bulma.color.isInfo - Bulma.button.isFullWidth - prop.onClick(fun e -> - let getUploadElement = Browser.Dom.document.getElementById inputId - getUploadElement.click() - ) - prop.text "Pick file names" - ] + match model.PersistentStorageState.Host with + | Some (Swatehost.ARCitect) -> + Html.div [ + prop.className "is-flex is-flex-direction-row" + prop.style [style.gap (length.rem 1)] + prop.children [ + Bulma.button.button [ + Bulma.color.isInfo + Bulma.button.isFullWidth + prop.onClick(fun e -> + ARCitect.RequestPaths false |> ARCitect.ARCitect.send + ) + prop.text "Pick Files" + ] + Bulma.button.button [ + Bulma.color.isInfo + Bulma.button.isFullWidth + prop.onClick(fun e -> + ARCitect.RequestPaths true |> ARCitect.ARCitect.send + ) + prop.text "Pick Directories" + ] + ] + ] + | _ -> + Bulma.button.button [ + Bulma.color.isInfo + Bulma.button.isFullWidth + prop.onClick(fun e -> + let getUploadElement = Browser.Dom.document.getElementById inputId + getUploadElement.click() + ) + prop.text "Pick file names" + ] ] let insertButton (model:Messages.Model) dispatch = @@ -122,31 +113,31 @@ let sortButton icon msg = let fileSortElements (model:Messages.Model) dispatch = Bulma.field.div [ Bulma.buttons [ - Bulma.button.a [ - prop.title "Copy to Clipboard" - prop.onClick(fun e -> - CustomComponents.ResponsiveFA.triggerResponsiveReturnEle "clipboard_filepicker" - let txt = model.FilePickerState.FileNames |> List.map snd |> String.concat System.Environment.NewLine - let textArea = Browser.Dom.document.createElement "textarea" - textArea?value <- txt - textArea?style?top <- "0" - textArea?style?left <- "0" - textArea?style?position <- "fixed" - - Browser.Dom.document.body.appendChild textArea |> ignore - - textArea.focus() - // Can't belive this actually worked - textArea?select() - - let t = Browser.Dom.document.execCommand("copy") - Browser.Dom.document.body.removeChild(textArea) |> ignore - () - ) - prop.children [ - CustomComponents.ResponsiveFA.responsiveReturnEle "clipboard_filepicker" "fa-regular fa-clipboard" "fa-solid fa-check" - ] - ] + //Bulma.button.a [ + // prop.title "Copy to Clipboard" + // prop.onClick(fun e -> + // CustomComponents.ResponsiveFA.triggerResponsiveReturnEle "clipboard_filepicker" + // let txt = model.FilePickerState.FileNames |> List.map snd |> String.concat System.Environment.NewLine + // let textArea = Browser.Dom.document.createElement "textarea" + // textArea?value <- txt + // textArea?style?top <- "0" + // textArea?style?left <- "0" + // textArea?style?position <- "fixed" + + // Browser.Dom.document.body.appendChild textArea |> ignore + + // textArea.focus() + // // Can't belive this actually worked + // textArea?select() + + // let t = Browser.Dom.document.execCommand("copy") + // Browser.Dom.document.body.removeChild(textArea) |> ignore + // () + // ) + // prop.children [ + // CustomComponents.ResponsiveFA.responsiveReturnEle "clipboard_filepicker" "fa-solid fa-copy" "fa-solid fa-check" + // ] + //] Bulma.buttons [ Bulma.buttons.hasAddons @@ -255,12 +246,10 @@ module FileNameTable = ] -let fileContainer (model:Messages.Model) dispatch inputId= +let fileContainer (model:Messages.Model) dispatch = mainFunctionContainer [ - Bulma.help "Choose one or multiple files, rearrange them and add their names to the Excel sheet." - - uploadButton model dispatch inputId + uploadButton model dispatch if model.FilePickerState.FileNames <> [] then fileSortElements model dispatch @@ -271,12 +260,11 @@ let fileContainer (model:Messages.Model) dispatch inputId= ] let filePickerComponent (model:Messages.Model) (dispatch:Messages.Msg -> unit) = - let inputId = "filePicker_OnFilePickerMainFunc" Bulma.content [ pageHeader "File Picker" Bulma.label "Select files from your computer and insert their names into Excel" // Colored container element for all uploaded file names and sort elements - fileContainer model dispatch inputId + fileContainer model dispatch ] \ No newline at end of file diff --git a/src/Client/Pages/JsonExporter/JsonExporter.fs b/src/Client/Pages/JsonExporter/JsonExporter.fs index e0f2d8fc..4a73cf99 100644 --- a/src/Client/Pages/JsonExporter/JsonExporter.fs +++ b/src/Client/Pages/JsonExporter/JsonExporter.fs @@ -11,7 +11,6 @@ open ExcelColors open Model open Messages -open JsonExporter open Browser.Dom @@ -25,422 +24,371 @@ let download(filename, text) = element.click(); - document.body.removeChild(element); - -let update (msg:JsonExporter.Msg) (currentModel: Messages.Model) : Messages.Model * Cmd<Messages.Msg> = - match msg with - // Style - | UpdateLoading isLoading -> - let nextModel = { currentModel with Messages.Model.JsonExporterModel.Loading = isLoading } - nextModel, Cmd.none - | UpdateShowTableExportTypeDropdown nextVal -> - let nextModel = { - currentModel.JsonExporterModel with - ShowTableExportTypeDropdown = nextVal - ShowWorkbookExportTypeDropdown = false - ShowXLSXExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateShowWorkbookExportTypeDropdown nextVal -> - let nextModel = { - currentModel.JsonExporterModel with - ShowTableExportTypeDropdown = false - ShowWorkbookExportTypeDropdown = nextVal - ShowXLSXExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateShowXLSXExportTypeDropdown nextVal -> - let nextModel = { - currentModel.JsonExporterModel with - ShowTableExportTypeDropdown = false - ShowWorkbookExportTypeDropdown = false - ShowXLSXExportTypeDropdown = nextVal - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateTableJsonExportType nextType -> - let nextModel = { - currentModel.JsonExporterModel with - TableJsonExportType = nextType - ShowTableExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateWorkbookJsonExportType nextType -> - let nextModel = { - currentModel.JsonExporterModel with - WorkbookJsonExportType = nextType - ShowWorkbookExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateXLSXParsingExportType nextType -> - let nextModel = { - currentModel.JsonExporterModel with - XLSXParsingExportType = nextType - ShowXLSXExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | CloseAllDropdowns -> - let nextModel = { - currentModel.JsonExporterModel with - ShowTableExportTypeDropdown = false - ShowWorkbookExportTypeDropdown = false - ShowXLSXExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - // - | ParseTableOfficeInteropRequest -> - let nextModel = { - currentModel.JsonExporterModel with - Loading = true - } - let cmd = - Cmd.OfPromise.either - OfficeInterop.Core.getBuildingBlocksAndSheet - () - (ParseTableServerRequest >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByJsonExporterModel nextModel, cmd - | ParseTableServerRequest (worksheetName, buildingBlocks) -> - let nextModel = { - currentModel.JsonExporterModel with - CurrentExportType = Some currentModel.JsonExporterModel.TableJsonExportType - Loading = true - } - let api = - match currentModel.JsonExporterModel.TableJsonExportType with - | JsonExportType.Assay -> - Api.swateJsonAPIv1.parseAnnotationTableToAssayJson - | JsonExportType.ProcessSeq -> - Api.swateJsonAPIv1.parseAnnotationTableToProcessSeqJson - | anythingElse -> failwith $"Cannot parse \"{anythingElse.ToString()}\" with this endpoint." - let cmd = - Cmd.OfAsync.either - api - (worksheetName, buildingBlocks) - (ParseTableServerResponse >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByJsonExporterModel nextModel, cmd - // - | ParseTablesOfficeInteropRequest -> - let cmd = - Cmd.OfPromise.either - OfficeInterop.Core.getBuildingBlocksAndSheets - () - (ParseTablesServerRequest >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - currentModel, cmd - | ParseTablesServerRequest (worksheetBuildingBlocksTuple) -> - let nextModel = { - currentModel.JsonExporterModel with - CurrentExportType = Some currentModel.JsonExporterModel.WorkbookJsonExportType - Loading = true - } - let api = - match currentModel.JsonExporterModel.WorkbookJsonExportType with - | JsonExportType.ProcessSeq -> - Api.swateJsonAPIv1.parseAnnotationTablesToProcessSeqJson - | JsonExportType.Assay -> - Api.swateJsonAPIv1.parseAnnotationTablesToAssayJson - | anythingElse -> failwith $"Cannot parse \"{anythingElse.ToString()}\" with this endpoint." - let cmd = - Cmd.OfAsync.either - api - worksheetBuildingBlocksTuple - (ParseTableServerResponse >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - - currentModel.updateByJsonExporterModel nextModel, cmd - // - | ParseTableServerResponse parsedJson -> - let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") - let jsonName = Option.bind (fun x -> Some <| "_" + x.ToString()) currentModel.JsonExporterModel.CurrentExportType |> Option.defaultValue "" - let _ = download ($"{n}{jsonName}.json",parsedJson) - let nextModel = { - currentModel.JsonExporterModel with - Loading = false - CurrentExportType = None - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - // - | StoreXLSXByteArray byteArr -> - let nextModel = { - currentModel.JsonExporterModel with - XLSXByteArray = byteArr - } - currentModel.updateByJsonExporterModel nextModel , Cmd.none - | ParseXLSXToJsonRequest byteArr -> - let nextModel = { - currentModel.JsonExporterModel with - CurrentExportType = Some currentModel.JsonExporterModel.XLSXParsingExportType - Loading = true - } - let apif = - match currentModel.JsonExporterModel.XLSXParsingExportType with - | JsonExportType.ProcessSeq -> Api.isaDotNetCommonApi.toProcessSeqJsonStr - | JsonExportType.Assay -> Api.isaDotNetCommonApi.toAssayJsonStr - | JsonExportType.ProtocolTemplate -> Api.isaDotNetCommonApi.toSwateTemplateJsonStr - let cmd = - Cmd.OfAsync.either - apif - byteArr - (ParseXLSXToJsonResponse >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByJsonExporterModel nextModel, cmd - | ParseXLSXToJsonResponse jsonStr -> - let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") - let jsonName = Option.bind (fun x -> Some <| "_" + x.ToString()) currentModel.JsonExporterModel.CurrentExportType |> Option.defaultValue "" - let _ = download ($"{n}{jsonName}.json",jsonStr) - let nextModel = { - currentModel.JsonExporterModel with - Loading = false - } - - currentModel.updateByJsonExporterModel nextModel, Cmd.none + document.body.removeChild(element) |> ignore + () + +//open Messages +//open Feliz +//open Feliz.Bulma + +//let dropdownItem (exportType:JsonExportType) (model:Model) msg (isActive:bool) = +// Bulma.dropdownItem.a [ +// prop.tabIndex 0 +// prop.onClick (fun e -> +// e.stopPropagation() +// exportType |> msg +// ) +// prop.onKeyDown (fun k -> if (int k.which) = 13 then exportType |> msg) +// prop.children [ +// Html.span [ +// prop.className "has-tooltip-right has-tooltip-multiline" +// prop.custom ("data-tooltip", exportType.toExplanation) +// prop.style [style.fontSize(length.rem 1.1); style.paddingRight 10; style.textAlign.center; style.color NFDIColors.Yellow.Darker20] +// Html.i [prop.className "fa-solid fa-circle-info"] |> prop.children +// ] + +// Html.span (exportType.ToString()) +// ] +// ] + +//let parseTableToISAJsonEle (model:Model) (dispatch:Messages.Msg -> unit) = +// mainFunctionContainer [ +// Bulma.field.div [ +// Bulma.field.hasAddons +// prop.children [ +// Bulma.control.div [ +// Bulma.dropdown [ +// if model.JsonExporterModel.ShowTableExportTypeDropdown then Bulma.dropdown.isActive +// prop.children [ +// Bulma.dropdownTrigger [ +// Bulma.button.a [ +// prop.onClick(fun e -> e.stopPropagation(); UpdateShowTableExportTypeDropdown (not model.JsonExporterModel.ShowTableExportTypeDropdown) |> JsonExporterMsg |> dispatch ) +// prop.children [ +// span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.TableJsonExportType.ToString())] +// Html.i [prop.className "fa-solid fa-angle-down"] +// ] +// ] +// ] +// Bulma.dropdownMenu [ +// Bulma.dropdownContent [ +// let msg = (UpdateTableJsonExportType >> JsonExporterMsg >> dispatch) +// dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.TableJsonExportType = JsonExportType.Assay) +// dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.TableJsonExportType = JsonExportType.ProcessSeq) +// ] +// ] +// ] +// ] +// ] +// Bulma.control.div [ +// Bulma.control.isExpanded +// Bulma.button.a [ +// Bulma.color.isInfo +// Bulma.button.isFullWidth +// prop.onClick(fun _ -> +// InterfaceMsg SpreadsheetInterface.ExportJsonTable |> dispatch +// ) +// prop.text "Download as isa json" +// ] |> prop.children +// ] +// ] +// ] +// ] + +//let parseTablesToISAJsonEle (model:Model) (dispatch:Messages.Msg -> unit) = +// mainFunctionContainer [ +// Bulma.field.div [ +// Bulma.field.hasAddons +// prop.children [ +// Bulma.control.div [ +// Bulma.dropdown [ +// if model.JsonExporterModel.ShowWorkbookExportTypeDropdown then Bulma.dropdown.isActive +// prop.children [ +// Bulma.dropdownTrigger [ +// Bulma.button.a [ +// prop.onClick (fun e -> e.stopPropagation(); UpdateShowWorkbookExportTypeDropdown (not model.JsonExporterModel.ShowWorkbookExportTypeDropdown) |> JsonExporterMsg |> dispatch ) +// prop.children [ +// span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.WorkbookJsonExportType.ToString())] +// Html.i [prop.className "fa-solid fa-angle-down"] +// ] +// ] +// ] +// Bulma.dropdownMenu [ +// Bulma.dropdownContent [ +// let msg = (UpdateWorkbookJsonExportType >> JsonExporterMsg >> dispatch) +// dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.WorkbookJsonExportType = JsonExportType.Assay) +// dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.WorkbookJsonExportType = JsonExportType.ProcessSeq) +// ] +// ] +// ] +// ] +// ] +// Bulma.control.div [ +// Bulma.control.isExpanded +// Bulma.button.a [ +// Bulma.color.isInfo +// Bulma.button.isFullWidth +// prop.onClick(fun _ -> +// InterfaceMsg SpreadsheetInterface.ExportJsonTables |> dispatch +// ) +// prop.text "Download as isa json" +// ] +// |> prop.children +// ] +// ] +// ] +// ] + +//// SND ELEMENT +//open Browser.Types + +//let fileUploadButton (model:Model) dispatch (id: string) = +// Bulma.label [ +// prop.className "mb-2 has-text-weight-normal" +// prop.children [ +// Bulma.fileInput [ +// prop.id id +// prop.type' "file"; +// prop.style [style.display.none] +// prop.onChange (fun (ev: File list) -> +// let files = ev//: Browser.Types.FileList = ev.target?files + +// let blobs = +// files +// |> List.map (fun f -> f.slice() ) + +// let reader = Browser.Dom.FileReader.Create() + +// reader.onload <- fun evt -> +// let byteArr = +// let arraybuffer : Fable.Core.JS.ArrayBuffer = evt.target?result +// let uintArr = Fable.Core.JS.Constructors.Uint8Array.Create arraybuffer +// uintArr.ToString().Split([|","|], System.StringSplitOptions.RemoveEmptyEntries) +// |> Array.map (fun byteStr -> byte byteStr) + +// StoreXLSXByteArray byteArr |> JsonExporterMsg |> dispatch + +// reader.onerror <- fun evt -> +// curry GenericLog Cmd.none ("Error", evt.Value) |> DevMsg |> dispatch + +// reader.readAsArrayBuffer(blobs |> List.head) + +// let picker = Browser.Dom.document.getElementById(id) +// // https://stackoverflow.com/questions/3528359/html-input-type-file-file-selection-event/3528376 +// picker?value <- null +// ) +// ] +// Bulma.button.a [ +// Bulma.color.isInfo; +// Bulma.button.isFullWidth +// prop.text "Upload Excel file" +// ] +// ] +// ] + + +//let xlsxUploadAndParsingMainElement (model:Model) (dispatch: Msg -> unit) = +// let inputId = "xlsxConverter_uploadButton" +// mainFunctionContainer [ +// // Upload xlsx file to byte [] +// fileUploadButton model dispatch inputId +// // Request parsing +// Bulma.field.div [ +// Bulma.field.hasAddons +// prop.children [ +// Bulma.control.div [ +// Bulma.dropdown [ +// if model.JsonExporterModel.ShowXLSXExportTypeDropdown then Bulma.dropdown.isActive +// prop.children [ +// Bulma.dropdownTrigger [ +// Bulma.button.a [ +// prop.onClick (fun e -> e.stopPropagation(); UpdateShowXLSXExportTypeDropdown (not model.JsonExporterModel.ShowXLSXExportTypeDropdown) |> JsonExporterMsg |> dispatch ) +// prop.children [ +// span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.XLSXParsingExportType.ToString())] +// Html.i [prop.className "fa-solid fa-angle-down"] +// ] +// ] +// ] +// Bulma.dropdownMenu [ +// Bulma.dropdownContent [ +// let msg = (UpdateXLSXParsingExportType >> JsonExporterMsg >> dispatch) +// dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.Assay) +// dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.ProcessSeq) +// dropdownItem JsonExportType.ProtocolTemplate model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.ProtocolTemplate) +// ] +// ] +// ] +// ] +// ] +// Bulma.control.div [ +// Bulma.control.isExpanded +// Bulma.button.a [ +// let hasContent = model.JsonExporterModel.XLSXByteArray <> Array.empty +// Bulma.color.isInfo +// if hasContent then +// Bulma.button.isActive +// else +// Bulma.color.isDanger +// prop.disabled true +// Bulma.button.isFullWidth +// prop.onClick(fun _ -> +// if hasContent then +// ParseXLSXToJsonRequest model.JsonExporterModel.XLSXByteArray |> JsonExporterMsg |> dispatch +// ) +// prop.text "Download as isa json" +// ] +// |> prop.children +// ] +// ] +// ] +// ] + +//let jsonExporterMainElement (model:Messages.Model) (dispatch: Messages.Msg -> unit) = + + //Bulma.content [ + + // prop.onSubmit (fun e -> e.preventDefault()) + // prop.onKeyDown (fun k -> if (int k.which) = 13 then k.preventDefault()) + // prop.onClick (fun e -> CloseAllDropdowns |> JsonExporterMsg |> dispatch) + // prop.style [style.minHeight(length.vh 100)] + // prop.children [ + // Bulma.label "Json Exporter" + + // Bulma.help [ + // str "Export swate annotation tables to " + // a [Href @"https://en.wikipedia.org/wiki/JSON"] [str "JSON"] + // str " format. Official ISA-JSON types can be found " + // a [Href @"https://isa-specs.readthedocs.io/en/latest/isajson.html#"] [str "here"] + // str "." + // ] + + // Bulma.label "Export active table" + + // parseTableToISAJsonEle model dispatch + + // Bulma.label "Export workbook" + + // parseTablesToISAJsonEle model dispatch + + // Bulma.label "Export Swate conform xlsx file." + + // xlsxUploadAndParsingMainElement model dispatch + // ] + //] -open Messages open Feliz open Feliz.Bulma -let dropdownItem (exportType:JsonExportType) (model:Model) msg (isActive:bool) = - Bulma.dropdownItem.a [ - prop.tabIndex 0 - prop.onClick (fun e -> - e.stopPropagation() - exportType |> msg - ) - prop.onKeyDown (fun k -> if (int k.which) = 13 then exportType |> msg) - prop.children [ - Html.span [ - prop.className "has-tooltip-right has-tooltip-multiline" - prop.custom ("data-tooltip", exportType.toExplanation) - prop.style [style.fontSize(length.rem 1.1); style.paddingRight 10; style.textAlign.center; style.color NFDIColors.Yellow.Darker20] - Html.i [prop.className "fa-solid fa-circle-info"] |> prop.children - ] +type private JsonExportState = { + ExportFormat: JsonExportFormat +} with + static member init() = { + ExportFormat = JsonExportFormat.ROCrate + } + +type FileExporter = - Html.span (exportType.ToString()) + + static member private FileFormat(efm: JsonExportFormat, state: JsonExportState, setState) = + let isSelected = efm = state.ExportFormat + Html.option [ + prop.selected isSelected + prop.text (string efm) ] - ] - -let parseTableToISAJsonEle (model:Model) (dispatch:Messages.Msg -> unit) = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.field.hasAddons - prop.children [ - Bulma.control.div [ - Bulma.dropdown [ - if model.JsonExporterModel.ShowTableExportTypeDropdown then Bulma.dropdown.isActive - prop.children [ - Bulma.dropdownTrigger [ - Bulma.button.a [ - prop.onClick(fun e -> e.stopPropagation(); UpdateShowTableExportTypeDropdown (not model.JsonExporterModel.ShowTableExportTypeDropdown) |> JsonExporterMsg |> dispatch ) + + [<ReactComponent>] + static member JsonExport(model: Messages.Model, dispatch) = + let state, setState = React.useState JsonExportState.init + Html.div [ + Bulma.field.div [ + Bulma.field.hasAddons + prop.children [ + Bulma.control.p [ + Html.span [ + prop.className "select" + prop.children [ + Html.select [ + prop.onChange (fun (e:Browser.Types.Event) -> + let jef: JsonExportFormat = JsonExportFormat.fromString (e.target?value) + { state with + ExportFormat = jef } + |> setState + ) prop.children [ - span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.TableJsonExportType.ToString())] - Html.i [prop.className "fa-solid fa-angle-down"] + FileExporter.FileFormat(JsonExportFormat.ROCrate, state, setState) + FileExporter.FileFormat(JsonExportFormat.ISA, state, setState) + FileExporter.FileFormat(JsonExportFormat.ARCtrl, state, setState) + FileExporter.FileFormat(JsonExportFormat.ARCtrlCompressed, state, setState) ] ] ] - Bulma.dropdownMenu [ - Bulma.dropdownContent [ - let msg = (UpdateTableJsonExportType >> JsonExporterMsg >> dispatch) - dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.TableJsonExportType = JsonExportType.Assay) - dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.TableJsonExportType = JsonExportType.ProcessSeq) - ] - ] ] ] - ] - Bulma.control.div [ - Bulma.control.isExpanded - Bulma.button.a [ - Bulma.color.isInfo - Bulma.button.isFullWidth - prop.onClick(fun _ -> - InterfaceMsg SpreadsheetInterface.ExportJsonTable |> dispatch - ) - prop.text "Download as isa json" - ] |> prop.children - ] - ] - ] - ] - -let parseTablesToISAJsonEle (model:Model) (dispatch:Messages.Msg -> unit) = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.field.hasAddons - prop.children [ - Bulma.control.div [ - Bulma.dropdown [ - if model.JsonExporterModel.ShowWorkbookExportTypeDropdown then Bulma.dropdown.isActive + Bulma.control.p [ + Bulma.control.isExpanded prop.children [ - Bulma.dropdownTrigger [ - Bulma.button.a [ - prop.onClick (fun e -> e.stopPropagation(); UpdateShowWorkbookExportTypeDropdown (not model.JsonExporterModel.ShowWorkbookExportTypeDropdown) |> JsonExporterMsg |> dispatch ) - prop.children [ - span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.WorkbookJsonExportType.ToString())] - Html.i [prop.className "fa-solid fa-angle-down"] - ] - ] - ] - Bulma.dropdownMenu [ - Bulma.dropdownContent [ - let msg = (UpdateWorkbookJsonExportType >> JsonExporterMsg >> dispatch) - dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.WorkbookJsonExportType = JsonExportType.Assay) - dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.WorkbookJsonExportType = JsonExportType.ProcessSeq) - ] + Bulma.button.button [ + Bulma.button.isFullWidth + prop.text "Download" + prop.onClick (fun _ -> + if model.SpreadsheetModel.ArcFile.IsSome then + SpreadsheetInterface.ExportJson (model.SpreadsheetModel.ArcFile.Value, state.ExportFormat) |> InterfaceMsg |> dispatch + ) ] ] ] ] - Bulma.control.div [ - Bulma.control.isExpanded - Bulma.button.a [ - Bulma.color.isInfo - Bulma.button.isFullWidth - prop.onClick(fun _ -> - InterfaceMsg SpreadsheetInterface.ExportJsonTables |> dispatch - ) - prop.text "Download as isa json" - ] - |> prop.children - ] ] ] - ] - -// SND ELEMENT -open Browser.Types - -let fileUploadButton (model:Model) dispatch (id: string) = - Bulma.label [ - prop.className "mb-2 has-text-weight-normal" - prop.children [ - Bulma.fileInput [ - prop.id id - prop.type' "file"; - prop.style [style.display.none] - prop.onChange (fun (ev: File list) -> - let files = ev//: Browser.Types.FileList = ev.target?files - - let blobs = - files - |> List.map (fun f -> f.slice() ) - - let reader = Browser.Dom.FileReader.Create() - - reader.onload <- fun evt -> - let byteArr = - let arraybuffer : Fable.Core.JS.ArrayBuffer = evt.target?result - let uintArr = Fable.Core.JS.Constructors.Uint8Array.Create arraybuffer - uintArr.ToString().Split([|","|], System.StringSplitOptions.RemoveEmptyEntries) - |> Array.map (fun byteStr -> byte byteStr) - - StoreXLSXByteArray byteArr |> JsonExporterMsg |> dispatch - - reader.onerror <- fun evt -> - curry GenericLog Cmd.none ("Error", evt.Value) |> DevMsg |> dispatch - - reader.readAsArrayBuffer(blobs |> List.head) - let picker = Browser.Dom.document.getElementById(id) - // https://stackoverflow.com/questions/3528359/html-input-type-file-file-selection-event/3528376 - picker?value <- null - ) - ] - Bulma.button.a [ - Bulma.color.isInfo; - Bulma.button.isFullWidth - prop.text "Upload Excel file" - ] - ] - ] - - -let xlsxUploadAndParsingMainElement (model:Model) (dispatch: Msg -> unit) = - let inputId = "xlsxConverter_uploadButton" - mainFunctionContainer [ - // Upload xlsx file to byte [] - fileUploadButton model dispatch inputId - // Request parsing - Bulma.field.div [ - Bulma.field.hasAddons - prop.children [ - Bulma.control.div [ - Bulma.dropdown [ - if model.JsonExporterModel.ShowXLSXExportTypeDropdown then Bulma.dropdown.isActive - prop.children [ - Bulma.dropdownTrigger [ - Bulma.button.a [ - prop.onClick (fun e -> e.stopPropagation(); UpdateShowXLSXExportTypeDropdown (not model.JsonExporterModel.ShowXLSXExportTypeDropdown) |> JsonExporterMsg |> dispatch ) - prop.children [ - span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.XLSXParsingExportType.ToString())] - Html.i [prop.className "fa-solid fa-angle-down"] - ] + static member Main(model:Messages.Model, dispatch: Messages.Msg -> unit) = + Html.div [ + pageHeader "File Export" + + Bulma.label "Export to Json" + mainFunctionContainer [ + Bulma.field.div [ + Bulma.content [ + Bulma.help "Export Swate annotation tables to official JSON." + Html.ul [ + Html.li [ + Html.b "ARCtrl" + str ": A simple ARCtrl specific format." + ] + Html.li [ + Html.b "ARCtrlCompressed" + str ": A compressed ARCtrl specific format." + ] + Html.li [ + Html.b "ISA" + str ": ISA-JSON format (" + Html.a [ + prop.target.blank + prop.href "https://isa-specs.readthedocs.io/en/latest/isajson.html#" + prop.text "ISA-JSON" ] + str ")." ] - Bulma.dropdownMenu [ - Bulma.dropdownContent [ - let msg = (UpdateXLSXParsingExportType >> JsonExporterMsg >> dispatch) - dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.Assay) - dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.ProcessSeq) - dropdownItem JsonExportType.ProtocolTemplate model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.ProtocolTemplate) + Html.li [ + Html.b "ROCrate" + str ": ROCrate format (" + Html.a [ + prop.target.blank + prop.href "https://www.researchobject.org/ro-crate/" + prop.text "ROCrate" + ] + str ", " + Html.a [ + prop.target.blank + prop.href "https://github.com/nfdi4plants/isa-ro-crate-profile/blob/main/profile/isa_ro_crate.md" + prop.text "ISA-Profile" ] + str ")." ] ] ] ] - Bulma.control.div [ - Bulma.control.isExpanded - Bulma.button.a [ - let hasContent = model.JsonExporterModel.XLSXByteArray <> Array.empty - Bulma.color.isInfo - if hasContent then - Bulma.button.isActive - else - Bulma.color.isDanger - prop.disabled true - Bulma.button.isFullWidth - prop.onClick(fun _ -> - if hasContent then - ParseXLSXToJsonRequest model.JsonExporterModel.XLSXByteArray |> JsonExporterMsg |> dispatch - ) - prop.text "Download as isa json" - ] - |> prop.children - ] + FileExporter.JsonExport(model, dispatch) ] ] - ] - - -let jsonExporterMainElement (model:Messages.Model) (dispatch: Messages.Msg -> unit) = - - Bulma.content [ - - prop.onSubmit (fun e -> e.preventDefault()) - prop.onKeyDown (fun k -> if (int k.which) = 13 then k.preventDefault()) - prop.onClick (fun e -> CloseAllDropdowns |> JsonExporterMsg |> dispatch) - prop.style [style.minHeight(length.vh 100)] - prop.children [ - Bulma.label "Json Exporter" - - Bulma.help [ - str "Export swate annotation tables to " - a [Href @"https://en.wikipedia.org/wiki/JSON"] [str "JSON"] - str " format. Official ISA-JSON types can be found " - a [Href @"https://isa-specs.readthedocs.io/en/latest/isajson.html#"] [str "here"] - str "." - ] - - Bulma.label "Export active table" - - parseTableToISAJsonEle model dispatch - Bulma.label "Export workbook" - - parseTablesToISAJsonEle model dispatch - - Bulma.label "Export Swate conform xlsx file." - xlsxUploadAndParsingMainElement model dispatch - ] - ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs similarity index 61% rename from src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs rename to src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs index b958e884..d4d5478b 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs @@ -7,7 +7,7 @@ open Messages open Feliz open Feliz.Bulma -let breadcrumbEle (model:Model) dispatch = +let private breadcrumbEle (model:Model) dispatch = Bulma.breadcrumb [ Bulma.breadcrumb.hasArrowSeparator prop.children [ @@ -30,11 +30,13 @@ let breadcrumbEle (model:Model) dispatch = open Fable.Core [<ReactComponent>] -let ProtocolSearchView (model:Model) dispatch = - React.useEffectOnce(fun () -> - Messages.Protocol.GetAllProtocolsRequest |> ProtocolMsg |> dispatch - ) - let isEmpty = model.ProtocolState.ProtocolsAll |> isNull || model.ProtocolState.ProtocolsAll |> Array.isEmpty +let Main (model:Model) dispatch = + let templates, setTemplates = React.useState(model.ProtocolState.Templates) + 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()) @@ -46,11 +48,11 @@ let ProtocolSearchView (model:Model) dispatch = if isEmpty && not isLoading then Bulma.help [Bulma.color.isDanger; prop.text "No templates were found. This can happen if connection to the server was lost. You can try reload this site or contact a developer."] - if isLoading then - Modals.Loading.loadingModal - Bulma.label "Search the database for protocol templates." - if not isEmpty then - Protocol.Component.ProtocolContainer model dispatch + mainFunctionContainer [ + Protocol.Search.InfoField() + Protocol.Search.FileSortElement(model, config, setConfig) + Protocol.Search.Component (filteredTemplates, model, dispatch) + ] ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs index 9cb14133..4eb25885 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs @@ -1,7 +1,6 @@ -module Protocol.Component +namespace Protocol open Shared -open TemplateTypes open Model open Messages.Protocol open Messages @@ -9,14 +8,9 @@ open Messages open Feliz open Feliz.Bulma -let private curatedOrganisationNames = [ - "dataplant" - "nfdi4plants" -] - /// Fields of Template that can be searched [<RequireQualifiedAccess>] -type private SearchFields = +type SearchFields = | Name | Organisation | Authors @@ -44,15 +38,14 @@ type private SearchFields = static member GetOfQuery(query:string) = SearchFields.ofFieldString query -open ARCtrl.ISA +open ARCtrl -type private ProtocolViewState = { - DisplayedProtDetailsId : int option +type TemplateFilterConfig = { ProtocolSearchQuery : string ProtocolTagSearchQuery : string ProtocolFilterTags : OntologyAnnotation list ProtocolFilterErTags : OntologyAnnotation list - CuratedCommunityFilter : Model.Protocol.CuratedCommunityFilter + CommunityFilter : Model.Protocol.CommunityFilter TagFilterIsAnd : bool Searchfield : SearchFields } with @@ -61,389 +54,387 @@ type private ProtocolViewState = { ProtocolTagSearchQuery = "" ProtocolFilterTags = [] ProtocolFilterErTags = [] - CuratedCommunityFilter = Model.Protocol.CuratedCommunityFilter.Both + CommunityFilter = Model.Protocol.CommunityFilter.OnlyCurated TagFilterIsAnd = true - DisplayedProtDetailsId = None Searchfield = SearchFields.Name } -[<LiteralAttribute>] -let private SearchFieldId = "template_searchfield_main" +module ComponentAux = -let private queryField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.column [ - Bulma.label $"Search by {state.Searchfield.toNameRdb}" - let hasSearchAddon = state.Searchfield <> SearchFields.Name - Bulma.field.div [ - if hasSearchAddon then Bulma.field.hasAddons - prop.children [ - if hasSearchAddon then + let curatedOrganisationNames = [ + "dataplant" + "nfdi4plants" + ] + + [<LiteralAttribute>] + let SearchFieldId = "template_searchfield_main" + + let queryField (model:Model) (state: TemplateFilterConfig) (setState: TemplateFilterConfig -> unit) = + Html.div [ + Bulma.label $"Search by {state.Searchfield.toNameRdb}" + let hasSearchAddon = state.Searchfield <> SearchFields.Name + Bulma.field.div [ + if hasSearchAddon then Bulma.field.hasAddons + prop.children [ + if hasSearchAddon then + Bulma.control.div [ + Bulma.button.a [ Bulma.button.isStatic; prop.text state.Searchfield.toStr] + ] Bulma.control.div [ - Bulma.button.a [ Bulma.button.isStatic; prop.text state.Searchfield.toStr] - ] - Bulma.control.div [ - Bulma.control.hasIconsRight - prop.children [ - Bulma.input.text [ - prop.placeholder $".. {state.Searchfield.toNameRdb}" - prop.id SearchFieldId - Bulma.color.isPrimary - prop.valueOrDefault state.ProtocolSearchQuery - prop.onChange (fun (e: string) -> - let query = e - // if query starts with "/" expect intend to search by different field - if query.StartsWith "/" then - let searchField = SearchFields.GetOfQuery query - if searchField.IsSome then - {state with Searchfield = searchField.Value; ProtocolSearchQuery = ""} |> setState - //let inp = Browser.Dom.document.getElementById SearchFieldId - // if query starts NOT with "/" update query - else - { - state with - ProtocolSearchQuery = query - DisplayedProtDetailsId = None - } - |> setState - ) + Bulma.control.hasIconsRight + prop.children [ + Bulma.input.text [ + prop.style [style.minWidth 200] + prop.placeholder $".. {state.Searchfield.toNameRdb}" + prop.id SearchFieldId + Bulma.color.isPrimary + prop.valueOrDefault state.ProtocolSearchQuery + prop.onChange (fun (e: string) -> + let query = e + // if query starts with "/" expect intend to search by different field + if query.StartsWith "/" then + let searchField = SearchFields.GetOfQuery query + if searchField.IsSome then + {state with Searchfield = searchField.Value; ProtocolSearchQuery = ""} |> setState + //let inp = Browser.Dom.document.getElementById SearchFieldId + // if query starts NOT with "/" update query + else + { + state with + ProtocolSearchQuery = query + } + |> setState + ) + ] + Bulma.icon [Bulma.icon.isSmall; Bulma.icon.isRight; prop.children (Html.i [prop.className "fa-solid fa-search"])] ] - Bulma.icon [Bulma.icon.isSmall; Bulma.icon.isRight; prop.children (Html.i [prop.className "fa-solid fa-search"])] ] ] ] ] - ] -let private tagQueryField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = - let allTags = model.ProtocolState.ProtocolsAll |> Array.collect (fun x -> x.Tags) |> Array.distinct |> Array.filter (fun x -> state.ProtocolFilterTags |> List.contains x |> not ) - let allErTags = model.ProtocolState.ProtocolsAll |> Array.collect (fun x -> x.EndpointRepositories) |> Array.distinct |> Array.filter (fun x -> state.ProtocolFilterErTags |> List.contains x |> not ) - let hitTagList, hitErTagList = - if state.ProtocolTagSearchQuery <> "" - then - let queryBigram = state.ProtocolTagSearchQuery |> Shared.SorensenDice.createBigrams - let getMatchingTags (allTags: OntologyAnnotation []) = - allTags - |> Array.map (fun x -> - x.NameText - |> Shared.SorensenDice.createBigrams - |> Shared.SorensenDice.calculateDistance queryBigram - , x - ) - |> Array.filter (fun x -> fst x >= 0.3 || (snd x).TermAccessionShort = state.ProtocolTagSearchQuery) - |> Array.sortByDescending fst - |> Array.map snd - let sortedTags = getMatchingTags allTags - let sortedErTags = getMatchingTags allErTags - sortedTags, sortedErTags - else - [||], [||] - Bulma.column [ - Bulma.label "Search for tags" - Bulma.control.div [ - Bulma.control.hasIconsRight - prop.children [ - Bulma.input.text [ - prop.placeholder ".. protocol tag" - Bulma.color.isPrimary - prop.valueOrDefault state.ProtocolTagSearchQuery - prop.onChange (fun (e:string) -> - {state with ProtocolTagSearchQuery = e} |> setState + let tagQueryField (model:Model) (state: TemplateFilterConfig) (setState: TemplateFilterConfig -> unit) = + let allTags = model.ProtocolState.Templates |> Seq.collect (fun x -> x.Tags) |> Seq.distinct |> Seq.filter (fun x -> state.ProtocolFilterTags |> List.contains x |> not ) |> Array.ofSeq + let allErTags = model.ProtocolState.Templates |> Seq.collect (fun x -> x.EndpointRepositories) |> Seq.distinct |> Seq.filter (fun x -> state.ProtocolFilterErTags |> List.contains x |> not ) |> Array.ofSeq + let hitTagList, hitErTagList = + if state.ProtocolTagSearchQuery <> "" + then + let queryBigram = state.ProtocolTagSearchQuery |> Shared.SorensenDice.createBigrams + let getMatchingTags (allTags: OntologyAnnotation []) = + allTags + |> Array.map (fun x -> + x.NameText + |> Shared.SorensenDice.createBigrams + |> Shared.SorensenDice.calculateDistance queryBigram + , x ) - ] - Bulma.icon [ - Bulma.icon.isSmall; Bulma.icon.isRight - Html.i [prop.className "fa-solid fa-search"] |> prop.children - ] - // Pseudo dropdown - Bulma.box [ - prop.style [ - style.position.absolute - style.width(length.perc 100) - style.zIndex 10 - if hitTagList |> Array.isEmpty && hitErTagList |> Array.isEmpty then style.display.none + |> Array.filter (fun x -> fst x >= 0.3 || (snd x).TermAccessionShort = state.ProtocolTagSearchQuery) + |> Array.sortByDescending fst + |> Array.map snd + let sortedTags = getMatchingTags allTags + let sortedErTags = getMatchingTags allErTags + sortedTags, sortedErTags + else + [||], [||] + Html.div [ + Bulma.label "Search for tags" + Bulma.control.div [ + Bulma.control.hasIconsRight + prop.children [ + Bulma.input.text [ + prop.style [style.minWidth 150] + prop.placeholder ".. protocol tag" + Bulma.color.isPrimary + prop.valueOrDefault state.ProtocolTagSearchQuery + prop.onChange (fun (e:string) -> + {state with ProtocolTagSearchQuery = e} |> setState + ) ] - prop.children [ - if hitErTagList <> [||] then - Bulma.label "Endpoint Repositories" - Bulma.tags [ - for tagSuggestion in hitErTagList do - yield - Bulma.tag [ - prop.className "clickableTag" - Bulma.color.isLink - prop.onClick (fun _ -> - let nextState = { - state with - ProtocolFilterErTags = tagSuggestion::state.ProtocolFilterErTags - ProtocolTagSearchQuery = "" - DisplayedProtDetailsId = None - } - setState nextState - ) - prop.title tagSuggestion.TermAccessionShort - prop.text tagSuggestion.NameText - ] - ] - if hitTagList <> [||] then - Bulma.label "Tags" - Bulma.tags [ - for tagSuggestion in hitTagList do - yield - Bulma.tag [ - prop.className "clickableTag" - Bulma.color.isInfo - prop.onClick (fun _ -> - let nextState = { + Bulma.icon [ + Bulma.icon.isSmall; Bulma.icon.isRight + Html.i [prop.className "fa-solid fa-search"] |> prop.children + ] + // Pseudo dropdown + Bulma.box [ + prop.style [ + style.position.absolute + style.width(length.perc 100) + style.zIndex 10 + if hitTagList |> Array.isEmpty && hitErTagList |> Array.isEmpty then style.display.none + ] + prop.children [ + if hitErTagList <> [||] then + Bulma.label "Endpoint Repositories" + Bulma.tags [ + for tagSuggestion in hitErTagList do + yield + Bulma.tag [ + prop.className "clickableTag" + Bulma.color.isLink + prop.onClick (fun _ -> + let nextState = { state with - ProtocolFilterTags = tagSuggestion::state.ProtocolFilterTags + ProtocolFilterErTags = tagSuggestion::state.ProtocolFilterErTags ProtocolTagSearchQuery = "" - DisplayedProtDetailsId = None } - setState nextState - //AddProtocolTag tagSuggestion |> ProtocolMsg |> dispatch - ) - prop.title tagSuggestion.TermAccessionShort - prop.text tagSuggestion.NameText - ] - ] + setState nextState + ) + prop.title tagSuggestion.TermAccessionShort + prop.text tagSuggestion.NameText + ] + ] + if hitTagList <> [||] then + Bulma.label "Tags" + Bulma.tags [ + for tagSuggestion in hitTagList do + yield + Bulma.tag [ + prop.className "clickableTag" + Bulma.color.isInfo + prop.onClick (fun _ -> + let nextState = { + state with + ProtocolFilterTags = tagSuggestion::state.ProtocolFilterTags + ProtocolTagSearchQuery = "" + } + setState nextState + //AddProtocolTag tagSuggestion |> ProtocolMsg |> dispatch + ) + prop.title tagSuggestion.TermAccessionShort + prop.text tagSuggestion.NameText + ] + ] + ] ] ] ] ] - ] -let private tagDisplayField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.columns [ - Bulma.columns.isMobile - prop.children [ - Bulma.column [ + open Fable.Core.JsInterop + + let communitySelectField (model: Messages.Model) (state: TemplateFilterConfig) setState = + let communityNames = + model.ProtocolState.Templates + |> Array.choose (fun t -> Model.Protocol.CommunityFilter.CommunityFromOrganisation t.Organisation) + |> Array.distinct |> List.ofArray + let options = + [ + Model.Protocol.CommunityFilter.All + Model.Protocol.CommunityFilter.OnlyCurated + ]@communityNames + Html.div [ + Bulma.label "Select community" + Bulma.control.div [ + Bulma.control.isExpanded + prop.children [ + Bulma.select [ + prop.value (state.CommunityFilter.ToStringRdb()) + prop.onChange(fun (e: Browser.Types.Event) -> + let filter = Model.Protocol.CommunityFilter.fromString e.target?value + if state.CommunityFilter <> filter then + {state with CommunityFilter = filter} |> setState + ) + prop.children [ + for option in options do + Html.option [ + //prop.selected (state.CommunityFilter = option) + prop.value (option.ToStringRdb()) + prop.text (option.ToStringRdb()) + ] + ] + ] + ] + ] + ] + + let TagRemovableElement (tag:OntologyAnnotation) (color: IReactProperty) (rmv: unit -> unit) = + Bulma.control.div [ + Bulma.tags [ + prop.style [style.flexWrap.nowrap] + Bulma.tags.hasAddons + prop.children [ + Bulma.tag [color; prop.style [style.borderWidth 0]; prop.text tag.NameText; prop.title tag.TermAccessionShort] + Bulma.tag [ + Bulma.tag.isDelete + prop.onClick (fun _ -> rmv()) + ] + ] + ] + ] + + let SwitchElement (tagIsFilterAnd: bool) (setFilter: bool -> unit) = + Html.div [ + prop.style [style.marginLeft length.auto] + prop.children [ + Bulma.button.button [ + Bulma.button.isSmall + prop.onClick (fun _ -> setFilter (not tagIsFilterAnd)) + prop.title (if tagIsFilterAnd then "Templates contain all tags." else "Templates contain at least one tag.") + prop.text (if tagIsFilterAnd then "And" else "Or") + ] + ] + ] + + let TagDisplayField (model:Model) (state: TemplateFilterConfig) (setState: TemplateFilterConfig -> unit) = + Html.div [ + prop.className "is-flex" + prop.children [ Bulma.field.div [ Bulma.field.isGroupedMultiline + prop.style [style.display.flex; style.flexGrow 1; style.gap (length.rem 0.5); style.flexWrap.wrap; style.flexDirection.row] prop.children [ for selectedTag in state.ProtocolFilterErTags do - yield Bulma.control.div [ - Bulma.tags [ - Bulma.tags.hasAddons - prop.children [ - Bulma.tag [Bulma.color.isLink; prop.style [style.borderWidth 0]; prop.text selectedTag.NameText] - Bulma.delete [ - prop.className "clickableTagDelete" - prop.onClick (fun _ -> - {state with ProtocolFilterErTags = state.ProtocolFilterErTags |> List.except [selectedTag]} |> setState - //RemoveProtocolErTag selectedTag |> ProtocolMsg |> dispatch - ) - ] - ] - ] - ] + let rmv = fun () -> {state with ProtocolFilterErTags = state.ProtocolFilterErTags |> List.except [selectedTag]} |> setState + TagRemovableElement selectedTag Bulma.color.isLink rmv for selectedTag in state.ProtocolFilterTags do - yield Bulma.control.div [ - Bulma.tags [ - Bulma.tags.hasAddons - prop.children [ - Bulma.tag [Bulma.color.isInfo; prop.style [style.borderWidth 0]; prop.text selectedTag.NameText] - Bulma.delete [ - prop.className "clickableTagDelete" - //Tag.Color IsWarning; - prop.onClick (fun _ -> - {state with ProtocolFilterTags = state.ProtocolFilterTags |> List.except [selectedTag]} |> setState - //RemoveProtocolTag selectedTag |> ProtocolMsg |> dispatch - ) - ] - ] - ] - ] + let rmv = fun () -> {state with ProtocolFilterTags = state.ProtocolFilterTags |> List.except [selectedTag]} |> setState + TagRemovableElement selectedTag Bulma.color.isInfo rmv ] ] - ] - // tag filter (AND or OR) - Bulma.column [ - Bulma.column.isNarrow - prop.title (if state.TagFilterIsAnd then "Templates contain all tags." else "Templates contain at least one tag.") - Switch.checkbox [ - Bulma.color.isDark - prop.style [style.userSelect.none] - switch.isOutlined - switch.isSmall - prop.id "switch-2" - prop.isChecked state.TagFilterIsAnd - prop.onChange (fun (e:bool) -> - {state with TagFilterIsAnd = not state.TagFilterIsAnd} |> setState - //UpdateTagFilterIsAnd (not state.TagFilterIsAnd) |> ProtocolMsg |> dispatch - ) - prop.children (if state.TagFilterIsAnd then Html.b "And" else Html.b "Or") - ] |> prop.children + // tag filter (AND or OR) + let filtersetter = fun b -> setState {state with TagFilterIsAnd = b} + SwitchElement state.TagFilterIsAnd filtersetter ] ] - ] -let private fileSortElements (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = + let fileSortElements (model:Model) (state: TemplateFilterConfig) (setState: TemplateFilterConfig -> unit) = - Html.div [ - prop.style [style.marginBottom(length.rem 0.75)] - prop.children [ - Bulma.columns [ - Bulma.columns.isMobile; prop.style [style.marginBottom 0] - prop.children [ - queryField model state setState - tagQueryField model state setState + Html.div [ + prop.style [style.marginBottom(length.rem 0.75); style.display.flex; style.flexDirection.column] + prop.children [ + Bulma.field.div [ + prop.className "template-filter-container" + prop.children [ + queryField model state setState + tagQueryField model state setState + communitySelectField model state setState + ] ] + // Only show the tag list and tag filter (AND or OR) if any tag exists + if state.ProtocolFilterErTags <> [] || state.ProtocolFilterTags <> [] then + Bulma.field.div [ + TagDisplayField model state setState + ] ] - // Only show the tag list and tag filter (AND or OR) if any tag exists - if state.ProtocolFilterErTags <> [] || state.ProtocolFilterTags <> [] then - tagDisplayField model state setState ] - ] -let private curatedTag = Bulma.tag [prop.text "curated"; Bulma.color.isSuccess] -let private communitytag = Bulma.tag [prop.text "community"; Bulma.color.isWarning] -let private curatedCommunityTag = - Bulma.tag [ - prop.style [style.custom("background", "linear-gradient(90deg, rgba(31,194,167,1) 50%, rgba(255,192,0,1) 50%)")] - Bulma.color.isSuccess - prop.children [ - Html.span [prop.style [style.marginRight (length.em 0.75)]; prop.text "cur"] - Html.span [prop.style [style.marginLeft (length.em 0.75); style.color "rgba(0, 0, 0, 0.7)"]; prop.text "com"] - ] - ] - -let createAuthorStringHelper (author: Person) = - let mi = if author.MidInitials.IsSome then author.MidInitials.Value else "" - $"{author.FirstName} {mi} {author.LastName}" -let createAuthorsStringHelper (authors: Person []) = authors |> Array.map createAuthorStringHelper |> String.concat ", " - -let private protocolElement i (template:ARCtrl.Template.Template) (model:Model) (state:ProtocolViewState) dispatch (setState: ProtocolViewState -> unit) = - let isActive = - match state.DisplayedProtDetailsId with - | Some id when id = i -> - true - | _ -> - false - [ - Html.tr [ - prop.key $"{i}_{template.Id}" - prop.classes [ "nonSelectText"; if isActive then "hoverTableEle"] - prop.style [ - style.cursor.pointer; style.userSelect.none; - ] - prop.onClick (fun e -> - e.preventDefault() - { state with - DisplayedProtDetailsId = if isActive then None else Some i } - |> setState - //if isActive then - // UpdateDisplayedProtDetailsId None |> ProtocolMsg |> dispatch - //else - // UpdateDisplayedProtDetailsId (Some i) |> ProtocolMsg |> dispatch - ) + let curatedTag = Bulma.tag [prop.text "curated"; Bulma.color.isSuccess] + let communitytag = Bulma.tag [prop.text "community"; Bulma.color.isWarning] + let curatedCommunityTag = + Bulma.tag [ + prop.style [style.custom("background", "linear-gradient(90deg, rgba(31,194,167,1) 50%, rgba(255,192,0,1) 50%)")] + Bulma.color.isSuccess prop.children [ - Html.td template.Name - Html.td ( - if curatedOrganisationNames |> List.contains (template.Organisation.ToString().ToLower()) then - curatedTag - else - communitytag - ) - //td [ Style [TextAlign TextAlignOptions.Center; VerticalAlign "middle"] ] [ a [ OnClick (fun e -> e.stopPropagation()); Href prot.DocsLink; Target "_Blank"; Title "docs" ] [Fa.i [Fa.Size Fa.Fa2x ; Fa.Regular.FileAlt] []] ] - Html.td [ prop.style [style.textAlign.center; style.verticalAlign.middle]; prop.text template.Version ] - //td [ Style [TextAlign TextAlignOptions.Center; VerticalAlign "middle"] ] [ str (string template.Used) ] - Html.td ( - Bulma.icon [Html.i [prop.className "fa-solid fa-chevron-down"]] - ) + Html.span [prop.style [style.marginRight (length.em 0.75)]; prop.text "cur"] + Html.span [prop.style [style.marginLeft (length.em 0.75); style.color "rgba(0, 0, 0, 0.7)"]; prop.text "com"] ] ] - Html.tr [ - Html.td [ + + let createAuthorStringHelper (author: Person) = + let mi = if author.MidInitials.IsSome then author.MidInitials.Value else "" + $"{author.FirstName} {mi} {author.LastName}" + let createAuthorsStringHelper (authors: ResizeArray<Person>) = authors |> Seq.map createAuthorStringHelper |> String.concat ", " + + let protocolElement i (template:ARCtrl.Template) (isShown:bool) (setIsShown: bool -> unit) (model:Model) dispatch = + [ + Html.tr [ + prop.key $"{i}_{template.Id}" + prop.classes [ "nonSelectText"; if isShown then "hoverTableEle"] prop.style [ - style.padding 0 - if isActive then - style.borderBottom (2, borderStyle.solid, "black") - else - style.display.none + style.cursor.pointer; style.userSelect.none; ] - prop.colSpan 4 + prop.onClick (fun e -> + e.preventDefault() + setIsShown (not isShown) + //if isActive then + // UpdateDisplayedProtDetailsId None |> ProtocolMsg |> dispatch + //else + // UpdateDisplayedProtDetailsId (Some i) |> ProtocolMsg |> dispatch + ) prop.children [ - Bulma.box [ - prop.style [style.borderRadius 0] + Html.td [ + prop.text template.Name + prop.key $"{i}_{template.Id}_name" + ] + Html.td [ + prop.key $"{i}_{template.Id}_tag" prop.children [ - Html.div [ - Html.div template.Description + if curatedOrganisationNames |> List.contains (template.Organisation.ToString().ToLower()) then + curatedTag + else + communitytag + ] + ] + //td [ Style [TextAlign TextAlignOptions.Center; VerticalAlign "middle"] ] [ a [ OnClick (fun e -> e.stopPropagation()); Href prot.DocsLink; Target "_Blank"; Title "docs" ] [Fa.i [Fa.Size Fa.Fa2x ; Fa.Regular.FileAlt] []] ] + Html.td [ prop.key $"{i}_{template.Id}_version"; prop.style [style.textAlign.center; style.verticalAlign.middle]; prop.text template.Version ] + //td [ Style [TextAlign TextAlignOptions.Center; VerticalAlign "middle"] ] [ str (string template.Used) ] + Html.td [ + prop.key $"{i}_{template.Id}_button" + prop.children [Bulma.icon [Html.i [prop.className "fa-solid fa-chevron-down"]] ] + ] + ] + ] + Html.tr [ + Html.td [ + prop.key $"{i}_{template.Id}_description" + prop.style [ + style.padding 0 + if isShown then + style.borderBottom (2, borderStyle.solid, "black") + else + style.display.none + ] + prop.colSpan 4 + prop.children [ + Bulma.box [ + prop.style [style.borderRadius 0] + prop.children [ Html.div [ - Html.div [ Html.b "Author: "; Html.span (createAuthorsStringHelper template.Authors) ] - Html.div [ Html.b "Created: "; Html.span (template.LastUpdated.ToString("yyyy/MM/dd")) ] + Html.div template.Description + Html.div [ + Html.div [ Html.b "Author: "; Html.span (createAuthorsStringHelper template.Authors) ] + Html.div [ Html.b "Created: "; Html.span (template.LastUpdated.ToString("yyyy/MM/dd")) ] + ] + Html.div [ + Html.div [ Html.b "Organisation: "; Html.span (template.Organisation.ToString()) ] + ] ] - Html.div [ - Html.div [ Html.b "Organisation: "; Html.span (template.Organisation.ToString()) ] + Bulma.tags [ + for tag in template.EndpointRepositories do + yield + Bulma.tag [Bulma.color.isLink; prop.text tag.NameText; prop.title tag.TermAccessionShort] + ] + Bulma.tags [ + for tag in template.Tags do + yield + Bulma.tag [Bulma.color.isInfo; prop.text tag.NameText; prop.title tag.TermAccessionShort] + ] + Bulma.button.a [ + prop.onClick (fun _ -> SelectProtocol template |> ProtocolMsg |> dispatch) + Bulma.button.isFullWidth; Bulma.color.isSuccess + prop.text "select" ] - ] - Bulma.tags [ - for tag in template.EndpointRepositories do - yield - Bulma.tag [Bulma.color.isLink; prop.text tag.NameText; prop.title tag.TermAccessionShort] - ] - Bulma.tags [ - for tag in template.Tags do - yield - Bulma.tag [Bulma.color.isInfo; prop.text tag.NameText; prop.title tag.TermAccessionShort] - ] - Bulma.button.a [ - prop.onClick (fun _ -> SelectProtocol template |> ProtocolMsg |> dispatch) - Bulma.button.isFullWidth; Bulma.color.isSuccess - prop.text "select" ] ] ] ] ] ] - ] -let private curatedCommunityFilterDropdownItem (filter:Protocol.CuratedCommunityFilter) (child: ReactElement) (state:ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.dropdownItem.a [ - prop.onClick(fun e -> - e.preventDefault(); - {state with CuratedCommunityFilter = filter} |> setState - //UpdateCuratedCommunityFilter filter |> ProtocolMsg |> dispatch - ) - prop.children child - ] - -let private curatedCommunityFilterElement (state:ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.dropdown [ - Bulma.dropdown.isHoverable - prop.children [ - Bulma.dropdownTrigger [ - Bulma.button.button [ - Bulma.button.isSmall; Bulma.button.isOutlined; Bulma.color.isWhite; - prop.style [style.padding 0] - match state.CuratedCommunityFilter with - | Protocol.CuratedCommunityFilter.Both -> curatedCommunityTag - | Protocol.CuratedCommunityFilter.OnlyCommunity -> communitytag - | Protocol.CuratedCommunityFilter.OnlyCurated -> curatedTag - |> prop.children - ] - ] - Bulma.dropdownMenu [ - prop.style [style.minWidth.unset; style.fontWeight.normal] - Bulma.dropdownContent [ - curatedCommunityFilterDropdownItem Protocol.CuratedCommunityFilter.Both curatedCommunityTag state setState - curatedCommunityFilterDropdownItem Protocol.CuratedCommunityFilter.OnlyCurated curatedTag state setState - curatedCommunityFilterDropdownItem Protocol.CuratedCommunityFilter.OnlyCommunity communitytag state setState - ] |> prop.children + let RefreshButton (model:Messages.Model) dispatch = + Bulma.button.button [ + Bulma.button.isSmall + prop.onClick (fun _ -> Messages.Protocol.GetAllProtocolsForceRequest |> ProtocolMsg |> dispatch) + prop.children [ + Bulma.icon [Html.i [prop.className "fa-solid fa-arrows-rotate"]] ] ] - ] - -open Feliz -open System -[<ReactComponent>] -let ProtocolContainer (model:Model) dispatch = +module FilterHelper = + open ComponentAux - let state, setState = React.useState(ProtocolViewState.init) - - let sortTableBySearchQuery (protocol: ARCtrl.Template.Template []) = - let query = state.ProtocolSearchQuery.Trim() + let sortTableBySearchQuery (searchfield: SearchFields) (searchQuery: string) (protocol: ARCtrl.Template []) = + let query = searchQuery.Trim() // Only search if field is not empty and does not start with "/". // If it starts with "/" and does not match SearchFields then it will never trigger search // As soon as it matches SearchFields it will be removed and can become 'query <> ""' @@ -458,58 +449,53 @@ let ProtocolContainer (model:Model) dispatch = protocol |> Array.map (fun template -> let score = - match state.Searchfield with + match searchfield with | SearchFields.Name -> createScore template.Name | SearchFields.Organisation -> createScore (template.Organisation.ToString()) | SearchFields.Authors -> let query' = query.ToLower() - let scores = template.Authors |> Array.filter (fun author -> + let scores = template.Authors |> Seq.filter (fun author -> (createAuthorStringHelper author).ToLower().Contains query' || (author.ORCID.IsSome && author.ORCID.Value = query) ) - if Array.isEmpty scores then 0.0 else 1.0 + if Seq.isEmpty scores then 0.0 else 1.0 score, template ) - |> Array.filter (fun (score,_) -> score > 0.1) + |> Array.filter (fun (score,_) -> score > 0.3) |> Array.sortByDescending fst + |> fun y -> + for score, x in y do + log (score, x.Name) + y |> Array.map snd scoredTemplate else protocol - let filterTableByTags (protocol:ARCtrl.Template.Template []) = - if state.ProtocolFilterTags <> [] || state.ProtocolFilterErTags <> [] then - protocol |> Array.filter (fun x -> - let tags = Array.append x.Tags x.EndpointRepositories |> Array.distinct - let filterTags = state.ProtocolFilterTags@state.ProtocolFilterErTags |> List.distinct - Seq.except filterTags tags - |> fun filteredTags -> - // if we want to filter by tag with AND, all tags must match - if state.TagFilterIsAnd then - Seq.length filteredTags = tags.Length - filterTags.Length - // if we want to filter by tag with OR, at least one tag must match - else - Seq.length filteredTags < tags.Length - ) + let filterTableByTags tags ertags tagfilter (templates: ARCtrl.Template []) = + if tags <> [] || ertags <> [] then + let tagArray = tags@ertags |> ResizeArray + let filteredTemplates = ResizeArray templates |> ARCtrl.Templates.filterByOntologyAnnotation(tagArray, tagfilter) + Array.ofSeq filteredTemplates else - protocol - let filterTableByCuratedCommunityFilter (protocol:ARCtrl.Template.Template []) = - match state.CuratedCommunityFilter with - | Protocol.CuratedCommunityFilter.Both -> protocol - | Protocol.CuratedCommunityFilter.OnlyCurated -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames) - | Protocol.CuratedCommunityFilter.OnlyCommunity -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames |> not) - - let sortedTable = - model.ProtocolState.ProtocolsAll - |> filterTableByTags - |> filterTableByCuratedCommunityFilter - |> sortTableBySearchQuery - |> Array.sortBy (fun template -> template.Name, template.Organisation) - - mainFunctionContainer [ + templates + let filterTableByCommunityFilter communityfilter (protocol:ARCtrl.Template []) = + match communityfilter with + | Protocol.CommunityFilter.All -> protocol + | Protocol.CommunityFilter.OnlyCurated -> protocol |> Array.filter (fun x -> x.Organisation.IsOfficial()) + | Protocol.CommunityFilter.Community name -> protocol |> Array.filter (fun x -> x.Organisation.ToString() = name) + +open Feliz +open System +open ComponentAux + + +type Search = + + static member InfoField() = Bulma.field.div [ - Bulma.help [ + Bulma.content [ Html.b "Search for templates." Html.span " For more information you can look " Html.a [ prop.href Shared.URLs.SwateWiki; prop.target "_Blank"; prop.text "here"] @@ -517,7 +503,7 @@ let ProtocolContainer (model:Model) dispatch = Html.a [ prop.href URLs.Helpdesk.UrlTemplateTopic; prop.target "_Blank"; prop.text "here"] Html.span "." ] - Bulma.help [ + Bulma.content [ Html.span "You can search by template name, organisation and authors. Just type:" Bulma.content [ Html.ul [ @@ -528,26 +514,74 @@ let ProtocolContainer (model:Model) dispatch = ] ] ] - fileSortElements model state setState - Bulma.table [ - Bulma.table.isFullWidth - Bulma.table.isStriped + + static member filterTemplates(templates: ARCtrl.Template [], config: TemplateFilterConfig) = + if templates.Length = 0 then [||] else + templates + |> Array.ofSeq + |> Array.sortBy (fun template -> template.Name, template.Organisation) + |> FilterHelper.filterTableByTags config.ProtocolFilterTags config.ProtocolFilterErTags config.TagFilterIsAnd + |> FilterHelper.filterTableByCommunityFilter config.CommunityFilter + |> FilterHelper.sortTableBySearchQuery config.Searchfield config.ProtocolSearchQuery + + [<ReactComponent>] + static member FileSortElement(model, config, configSetter: TemplateFilterConfig -> unit) = + fileSortElements model config configSetter + + [<ReactComponent>] + static member Component (templates, model:Model, dispatch, ?maxheight: Styles.ICssUnit) = + let maxheight = defaultArg maxheight (length.px 600) + let showIds, setShowIds = React.useState(fun _ -> []) + Html.div [ + prop.style [style.overflow.auto; style.maxHeight maxheight] prop.children [ - Html.thead [ - Html.tr [ - Html.th "Template Name" - //th [ Style [ Color model.SiteStyleState.ColorMode.Text; TextAlign TextAlignOptions.Center] ] [ str "Documentation" ] - Html.th [curatedCommunityFilterElement state setState] - Html.th "Template Version" - //th [ Style [ Color model.SiteStyleState.ColorMode.Text; TextAlign TextAlignOptions.Center] ] [ str "Uses" ] - Html.th Html.none + Bulma.table [ + Bulma.table.isFullWidth + Bulma.table.isStriped + prop.className "tableFixHead" + prop.children [ + Html.thead [ + Html.tr [ + Html.th "Template Name" + //th [ Style [ Color model.SiteStyleState.ColorMode.Text; TextAlign TextAlignOptions.Center] ] [ str "Documentation" ] + Html.th "Community"//[CommunityFilterElement state setState] + Html.th "Template Version" + //th [ Style [ Color model.SiteStyleState.ColorMode.Text; TextAlign TextAlignOptions.Center] ] [ str "Uses" ] + Html.th [ + RefreshButton model dispatch + ] + ] + ] + Html.tbody [ + match model.ProtocolState.Loading with + | true -> + Html.tr [ + Html.td [ + prop.colSpan 4 + prop.style [style.textAlign.center] + prop.children [ + Bulma.icon [ + Bulma.icon.isMedium + prop.children [ + Html.i [prop.className "fa-solid fa-spinner fa-spin fa-lg"] + ] + ] + ] + ] + ] + | false -> + match templates with + | [||] -> + Html.tr [ Html.td "Empty" ] + | _ -> + for i in 0 .. templates.Length-1 do + let isShown = showIds |> List.contains i + let setIsShown (show: bool) = + if show then i::showIds |> setShowIds else showIds |> List.filter (fun x -> x <> i) |> setShowIds + yield! + protocolElement i templates.[i] isShown setIsShown model dispatch + ] ] ] - Html.tbody [ - for i in 0 .. sortedTable.Length-1 do - yield! - protocolElement i sortedTable.[i] model state dispatch setState - ] ] - ] - ] \ No newline at end of file + ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolState.fs b/src/Client/Pages/ProtocolTemplates/ProtocolState.fs index 5f4dc290..9182297f 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolState.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolState.fs @@ -10,45 +10,53 @@ module Protocol = open Shared open Fable.Core - let update (fujMsg:Protocol.Msg) (currentState: Protocol.Model) : Protocol.Model * Cmd<Messages.Msg> = + let update (fujMsg:Protocol.Msg) (state: Protocol.Model) : Protocol.Model * Cmd<Messages.Msg> = match fujMsg with - // // ------ Process from file ------ - | ParseUploadedFileRequest bytes -> - let nextModel = { currentState with Loading = true } - failwith "ParseUploadedFileRequest IS NOT IMPLEMENTED YET" - //let cmd = - // Cmd.OfAsync.either - // Api.templateApi.tryParseToBuildingBlocks - // bytes - // (ParseUploadedFileResponse >> ProtocolMsg) - // (curry GenericError (UpdateLoading false |> ProtocolMsg |> Cmd.ofMsg) >> DevMsg) - nextModel, Cmd.none - | ParseUploadedFileResponse buildingBlockTables -> - let nextState = { currentState with UploadedFileParsed = buildingBlockTables; Loading = false } - nextState, Cmd.none + | UpdateLoading next -> + {state with Loading = next}, Cmd.none // ------ Protocol from Database ------ | GetAllProtocolsRequest -> - let nextState = {currentState with Loading = true} + let now = System.DateTime.UtcNow + let olderThanOneHour = state.LastUpdated |> Option.map (fun last -> (now - last) > System.TimeSpan(1,0,0)) + let cmd = + if olderThanOneHour.IsNone || olderThanOneHour.Value then GetAllProtocolsForceRequest |> ProtocolMsg |> Cmd.ofMsg else Cmd.none + state, cmd + | GetAllProtocolsForceRequest -> + let nextState = {state with Loading = true} let cmd = + let updateRequestStateOnErrorCmd = UpdateLoading false |> ProtocolMsg |> Cmd.ofMsg Cmd.OfAsync.either Api.templateApi.getTemplates () (GetAllProtocolsResponse >> ProtocolMsg) - (curry GenericError Cmd.none >> DevMsg) + (curry GenericError updateRequestStateOnErrorCmd >> DevMsg) nextState, cmd | GetAllProtocolsResponse protocolsJson -> - let protocols = protocolsJson |> Array.map (ARCtrl.Template.Json.Template.fromJsonString) + let state = {state with Loading = false} + let templates = + try + protocolsJson |> ARCtrl.Json.Templates.fromJsonString |> Ok + with + | e -> Result.Error e + let nextState, cmd = + match templates with + | Ok t0 -> + let t = Array.ofSeq t0 + let nextState = { state with LastUpdated = Some System.DateTime.UtcNow } + nextState, UpdateTemplates t |> ProtocolMsg |> Cmd.ofMsg + | Result.Error e -> state, GenericError (Cmd.none,e) |> DevMsg |> Cmd.ofMsg + nextState, cmd + | UpdateTemplates templates -> let nextState = { - currentState with - ProtocolsAll = protocols - Loading = false + state with + Templates = templates } nextState, Cmd.none | SelectProtocol prot -> let nextState = { - currentState with - ProtocolSelected = Some prot + state with + TemplateSelected = Some prot } nextState, Cmd.ofMsg (UpdatePageState <| Some Routing.Route.Protocol) | ProtocolIncreaseTimesUsed templateId -> @@ -58,20 +66,12 @@ module Protocol = // Api.templateApi.increaseTimesUsedById // templateId // (curry GenericError Cmd.none >> DevMsg) - currentState, Cmd.none + state, Cmd.none // Client - | UpdateLoading nextLoadingState -> - let nextState = { - currentState with Loading = nextLoadingState - } - nextState, Cmd.none | RemoveSelectedProtocol -> let nextState = { - currentState with - ProtocolSelected = None + state with + TemplateSelected = None } nextState, Cmd.none - | RemoveUploadedFileParsed -> - let nextState = {currentState with UploadedFileParsed = Array.empty} - nextState, Cmd.none diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs index 59925ed7..3a298d5b 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs @@ -1,4 +1,4 @@ -module Protocol.Core +namespace Protocol open System @@ -13,250 +13,31 @@ open Fable.Core.JsInterop open Model open Messages open Browser.Types - -open Shared - -open OfficeInterop -open Protocol - +open SpreadsheetInterface open Messages open Elmish open Feliz open Feliz.Bulma -module TemplateFromJsonFile = - - let fileUploadButton (model:Model) dispatch = - let uploadId = "UploadFiles_ElementId" - Bulma.label [ - Bulma.fileInput [ - prop.id uploadId - prop.type' "file"; - prop.style [style.display.none] - prop.onChange (fun (ev: File list) -> - let fileList = ev //: FileList = ev.target?files - - if fileList.Length > 0 then - let file = fileList.Item 0 |> fun f -> f.slice() - - let reader = Browser.Dom.FileReader.Create() - - reader.onload <- fun evt -> - let (r: byte []) = evt.target?result - r |> ParseUploadedFileRequest |> ProtocolMsg |> dispatch - - reader.onerror <- fun evt -> - curry GenericLog Cmd.none ("Error", evt.Value) |> DevMsg |> dispatch - - reader.readAsArrayBuffer(file) - else - () - let picker = Browser.Dom.document.getElementById(uploadId) - // https://stackoverflow.com/questions/3528359/html-input-type-file-file-selection-event/3528376 - picker?value <- null - ) - ] - Bulma.button.a [ - Bulma.color.isInfo; - Bulma.button.isFullWidth - prop.onClick(fun e -> - e.preventDefault() - let getUploadElement = Browser.Dom.document.getElementById uploadId - getUploadElement.click() - () - ) - prop.text "Upload protocol" - ] - ] - - let fileUploadEle (model:Model) dispatch = - let hasData = model.ProtocolState.UploadedFileParsed <> Array.empty - Bulma.columns [ - Bulma.columns.isMobile - prop.children [ - Bulma.column [ - fileUploadButton model dispatch - ] - if hasData then - Bulma.column [ - Bulma.column.isNarrow - Bulma.button.a [ - prop.onClick (fun e -> RemoveUploadedFileParsed |> ProtocolMsg |> dispatch) - Bulma.color.isDanger - prop.children (Html.i [prop.className "fa-solid fa-times"]) - ] |> prop.children - ] - ] - ] - - let importToTableEle (model:Model) (dispatch:Messages.Msg -> unit) = - let hasData = model.ProtocolState.UploadedFileParsed <> Array.empty - Bulma.field.div [ - Bulma.field.hasAddons - Bulma.control.div [ - Bulma.control.isExpanded - Bulma.button.a [ - Bulma.color.isInfo - if hasData then - Bulma.button.isActive - else - Bulma.color.isDanger - prop.disabled true - Bulma.button.isFullWidth - prop.onClick(fun _ -> - Browser.Dom.window.alert("'SpreadsheetInterface.ImportFile' is not implemented") - //SpreadsheetInterface.ImportFile model.ProtocolState.UploadedFileParsed |> InterfaceMsg |> dispatch - ) - prop.text "Insert json" - ] |> prop.children - ] |> prop.children - ] - - let protocolInsertElement (model:Model) dispatch = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.help [ - b [] [str "Insert tables via ISA-JSON files."] - str " You can use Swate.Experts to create these files from existing Swate tables. " - span [Style [Color NFDIColors.Red.Base]] [str "Only missing building blocks will be added."] - ] - ] - - Bulma.field.div [ - fileUploadEle model dispatch - ] - - importToTableEle model dispatch - ] - -module TemplateFromDB = - - let toProtocolSearchElement (model:Model) dispatch = - Bulma.button.span [ - prop.onClick(fun _ -> UpdatePageState (Some Routing.Route.ProtocolSearch) |> dispatch) - Bulma.color.isInfo - Bulma.button.isFullWidth - prop.style [style.margin (length.rem 1, length.px 0)] - prop.text "Browse database" ] - - let addFromDBToTableButton (model:Messages.Model) dispatch = - Bulma.columns [ - Bulma.columns.isMobile - prop.children [ - Bulma.column [ - prop.children [ - Bulma.button.a [ - Bulma.color.isSuccess - if model.ProtocolState.ProtocolSelected.IsSome then - Bulma.button.isActive - else - Bulma.color.isDanger - prop.disabled true - Bulma.button.isFullWidth - prop.onClick (fun _ -> - if model.ProtocolState.ProtocolSelected.IsNone then - failwith "No template selected!" - // Remove existing columns - let mutable columnsToRemove = [] - // find duplicate columns - let tablecopy = model.ProtocolState.ProtocolSelected.Value.Table.Copy() - for header in tablecopy.Headers do - let containsAtIndex = model.SpreadsheetModel.ActiveTable.Headers.FindIndex(fun h -> h = header) - if containsAtIndex >= 0 then - columnsToRemove <- containsAtIndex::columnsToRemove - tablecopy.RemoveColumns (Array.ofList columnsToRemove) - let index = Spreadsheet.Sidebar.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel - SpreadsheetInterface.JoinTable (tablecopy, Some index, Some ARCtrl.ISA.TableJoinOptions.WithUnit ) |> InterfaceMsg |> dispatch - ) - prop.text "Add template" - ] - ] - ] - if model.ProtocolState.ProtocolSelected.IsSome then - Bulma.column [ - Bulma.column.isNarrow - Bulma.button.a [ - prop.onClick (fun e -> RemoveSelectedProtocol |> ProtocolMsg |> dispatch) - Bulma.color.isDanger - Html.i [prop.className "fa-solid fa-times"] |> prop.children - ] |> prop.children - ] - ] - ] - - let displaySelectedProtocolEle (model:Model) dispatch = - [ - div [Style [OverflowX OverflowOptions.Auto; MarginBottom "1rem"]] [ - Bulma.table [ - Bulma.table.isFullWidth; - Bulma.table.isBordered - prop.children [ - thead [] [ - Html.tr [ - Html.th "Column" - Html.th "Column TAN" - //Html.th "Unit" - //Html.th "Unit TAN" - ] - ] - tbody [] [ - for column in model.ProtocolState.ProtocolSelected.Value.Table.Columns do - //let unitOption = column.TryGetColumnUnits() - yield - Html.tr [ - td [] [str (column.Header.ToString())] - td [] [str (if column.Header.IsTermColumn then column.Header.ToTerm().TermAccessionShort else "-")] - //td [] [str (if unitOption.IsSome then insertBB.UnitTerm.Value.Name else "-")] - //td [] [str (if insertBB.HasUnit then insertBB.UnitTerm.Value.TermAccession else "-")] - ] - ] - ] - ] - ] - addFromDBToTableButton model dispatch - ] - - - let showDatabaseProtocolTemplate (model:Messages.Model) dispatch = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.help [ - b [] [str "Search the database for templates."] - str " The building blocks from these templates can be inserted into the Swate table. " - span [Style [Color NFDIColors.Red.Base]] [str "Only missing building blocks will be added."] - ] - ] - Bulma.field.div [ - toProtocolSearchElement model dispatch - ] - - Bulma.field.div [ - addFromDBToTableButton model dispatch - ] - if model.ProtocolState.ProtocolSelected.IsSome then - Bulma.field.div [ - yield! displaySelectedProtocolEle model dispatch - ] - ] - +type Templates = -let fileUploadViewComponent (model:Messages.Model) dispatch = - div [ - OnSubmit (fun e -> e.preventDefault()) - // https://keycode.info/ - OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) - ] [ + static member Main (model:Messages.Model, dispatch) = + div [ + OnSubmit (fun e -> e.preventDefault()) + // https://keycode.info/ + OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) + ] [ - pageHeader "Templates" + pageHeader "Templates" - // Box 1 - Bulma.label "Add template from database." + // Box 1 + Bulma.label "Add template from database." - TemplateFromDB.showDatabaseProtocolTemplate model dispatch + TemplateFromDB.Main(model, dispatch) - //// Box 2 - //Bulma.label "Add template(s) from file." + // Box 2 + Bulma.label "Add template(s) from file." - //TemplateFromJsonFile.protocolInsertElement model dispatch - ] \ No newline at end of file + TemplateFromFile.Main(model, dispatch) + ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs new file mode 100644 index 00000000..b31c2299 --- /dev/null +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs @@ -0,0 +1,115 @@ +namespace Protocol + +open Feliz +open Feliz.Bulma +open Messages +open Shared + +type TemplateFromDB = + + static member toProtocolSearchElement (model:Model) dispatch = + Bulma.button.span [ + prop.onClick(fun _ -> UpdatePageState (Some Routing.Route.ProtocolSearch) |> dispatch) + Bulma.color.isInfo + Bulma.button.isFullWidth + prop.style [style.margin (length.rem 1, length.px 0)] + prop.text "Browse database" ] + + static member addFromDBToTableButton (model:Messages.Model) dispatch = + Bulma.columns [ + Bulma.columns.isMobile + prop.children [ + Bulma.column [ + prop.children [ + Bulma.button.a [ + Bulma.color.isSuccess + if model.ProtocolState.TemplateSelected.IsSome then + Bulma.button.isActive + else + Bulma.color.isDanger + prop.disabled true + Bulma.button.isFullWidth + prop.onClick (fun _ -> + if model.ProtocolState.TemplateSelected.IsNone then + failwith "No template selected!" + /// 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 + SpreadsheetInterface.JoinTable (preparedTemplate, Some index, Some joinConfig) |> InterfaceMsg |> dispatch + ) + prop.text "Add template" + ] + ] + ] + if model.ProtocolState.TemplateSelected.IsSome then + Bulma.column [ + Bulma.column.isNarrow + Bulma.button.a [ + prop.onClick (fun e -> Protocol.RemoveSelectedProtocol |> ProtocolMsg |> dispatch) + Bulma.color.isDanger + Html.i [prop.className "fa-solid fa-times"] |> prop.children + ] |> prop.children + ] + ] + ] + + static member displaySelectedProtocolEle (model:Model) dispatch = + Html.div [ + prop.style [style.overflowX.auto; style.marginBottom (length.rem 1)] + prop.children [ + Bulma.table [ + Bulma.table.isFullWidth; + Bulma.table.isBordered + prop.children [ + Html.thead [ + Html.tr [ + Html.th "Column" + Html.th "Column TAN" + //Html.th "Unit" + //Html.th "Unit TAN" + ] + ] + Html.tbody [ + for column in model.ProtocolState.TemplateSelected.Value.Table.Columns do + //let unitOption = column.TryGetColumnUnits() + yield + Html.tr [ + Html.td (column.Header.ToString()) + Html.td (if column.Header.IsTermColumn then column.Header.ToTerm().TermAccessionShort else "-") + //td [] [str (if unitOption.IsSome then insertBB.UnitTerm.Value.Name else "-")] + //td [] [str (if insertBB.HasUnit then insertBB.UnitTerm.Value.TermAccession else "-")] + ] + ] + ] + ] + ] + ] + + static member Main(model:Messages.Model, dispatch) = + mainFunctionContainer [ + Bulma.field.div [ + Bulma.help [ + Html.b "Search the database for templates." + Html.text " The building blocks from these templates can be inserted into the Swate table. " + Html.span [ + color.hasTextDanger + prop.text "Only missing building blocks will be added." + ] + ] + ] + Bulma.field.div [ + TemplateFromDB.toProtocolSearchElement model dispatch + ] + + Bulma.field.div [ + TemplateFromDB.addFromDBToTableButton model dispatch + ] + if model.ProtocolState.TemplateSelected.IsSome then + Bulma.field.div [ + TemplateFromDB.displaySelectedProtocolEle model dispatch + ] + Bulma.field.div [ + TemplateFromDB.addFromDBToTableButton model dispatch + ] + ] diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs new file mode 100644 index 00000000..1a36ea71 --- /dev/null +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs @@ -0,0 +1,178 @@ +namespace Protocol + +open Fable.Core +open Fable.React +open Fable.React.Props +//open Fable.Core.JS +open Fable.Core.JsInterop + +open Model +open Messages +open Browser.Types +open SpreadsheetInterface +open Messages +open Elmish + +open Feliz +open Feliz.Bulma +open Shared +open ARCtrl + +type private TemplateFromFileState = { + /// User select type to upload + FileType: ArcFilesDiscriminate + /// User selects json type to upload + JsonFormat: JsonExportFormat + UploadedFile: ArcFiles option + Loading: bool +} with + static member init () = + { + FileType = ArcFilesDiscriminate.Assay + JsonFormat = JsonExportFormat.ROCrate + UploadedFile = None + Loading = false + } + +module private Helper = + + let upload (uploadId: string) (state: TemplateFromFileState) setState dispatch (ev: File list) = + let fileList = ev //: FileList = ev.target?files + + if fileList.Length > 0 then + let file = fileList.Item 0 |> fun f -> f.slice() + + let reader = Browser.Dom.FileReader.Create() + + reader.onload <- fun evt -> + let (r: string) = evt.target?result + async { + setState {state with Loading = true} + let! af = Spreadsheet.IO.Json.readFromJson state.FileType state.JsonFormat r |> Async.AwaitPromise + setState {state with UploadedFile = Some af; Loading = false} + } |> Async.StartImmediate + + reader.onerror <- fun evt -> + curry GenericLog Cmd.none ("Error", evt.Value) |> DevMsg |> dispatch + + reader.readAsText(file) + else + () + let picker = Browser.Dom.document.getElementById(uploadId) + // https://stackoverflow.com/questions/3528359/html-input-type-file-file-selection-event/3528376 + picker?value <- null + +type TemplateFromFile = + + static member private fileUploadButton (state:TemplateFromFileState, setState: TemplateFromFileState -> unit, dispatch) = + let uploadId = "UploadFiles_ElementId" + Bulma.label [ + Bulma.fileInput [ + prop.id uploadId + prop.type' "file"; + prop.style [style.display.none] + prop.onChange (fun (ev: File list) -> + Helper.upload uploadId state setState dispatch ev + ) + ] + Bulma.button.a [ + Bulma.color.isInfo; + Bulma.button.isFullWidth + prop.onClick(fun e -> + e.preventDefault() + let getUploadElement = Browser.Dom.document.getElementById uploadId + getUploadElement.click() + ) + prop.text "Upload protocol" + ] + ] + + static member private SelectorButton<'a when 'a : equality> (targetselector: 'a, selector: 'a, setSelector: 'a -> unit, ?isDisabled) = + Bulma.button.button [ + if isDisabled.IsSome then + prop.disabled isDisabled.Value + prop.style [style.flexGrow 1] + if (targetselector = selector) then + color.isSuccess + button.isSelected + prop.onClick (fun _ -> setSelector targetselector) + prop.text (string targetselector) + ] + + [<ReactComponent>] + static member Main(model: Messages.Model, dispatch) = + let state, setState = React.useState(TemplateFromFileState.init) + let af = React.useRef ( + let a = ArcAssay.init("My Assay") + let t1 = a.InitTable("Template Table 1") + t1.AddColumns([| + CompositeColumn.create(CompositeHeader.Input IOType.Source, [| for i in 1 .. 5 do sprintf "Source _ %i" i |> CompositeCell.FreeText |]) + CompositeColumn.create(CompositeHeader.Output IOType.Sample, [| for i in 1 .. 5 do sprintf "Sample _ %i" i |> CompositeCell.FreeText |]) + CompositeColumn.create(CompositeHeader.Component (OntologyAnnotation("instrument model", "MS", "MS:19283")), [| for i in 1 .. 5 do OntologyAnnotation("SCIEX instrument model", "MS", "MS:21387189237") |> CompositeCell.Term |]) + CompositeColumn.create(CompositeHeader.Factor (OntologyAnnotation("temperature", "UO", "UO:21387")), [| for i in 1 .. 5 do CompositeCell.createUnitized("", OntologyAnnotation("degree celcius", "UO", "UO:21387189237")) |]) + |]) + let t2 = a.InitTable("Template Table 2") + t2.AddColumns([| + CompositeColumn.create(CompositeHeader.Input IOType.Source, [| for i in 1 .. 5 do sprintf "Source2 _ %i" i |> CompositeCell.FreeText |]) + CompositeColumn.create(CompositeHeader.Output IOType.Sample, [| for i in 1 .. 5 do sprintf "Sample2 _ %i" i |> CompositeCell.FreeText |]) + CompositeColumn.create(CompositeHeader.Component (OntologyAnnotation("instrument", "MS", "MS:19283")), [| for i in 1 .. 5 do OntologyAnnotation("SCIEX instrument model", "MS", "MS:21387189237") |> CompositeCell.Term |]) + CompositeColumn.create(CompositeHeader.Factor (OntologyAnnotation("temperature", "UO", "UO:21387")), [| for i in 1 .. 5 do CompositeCell.createUnitized("", OntologyAnnotation("degree celcius", "UO", "UO:21387189237")) |]) + |]) + let af = ArcFiles.Assay a + af + ) + let setJsonFormat = fun x -> setState { state with JsonFormat = x } + let setFileType = fun x -> setState { state with FileType = x } + let fileTypeDisabled (ft: ArcFilesDiscriminate) = + match state.JsonFormat, ft with + // isa and rocrate do not support template + | JsonExportFormat.ROCrate, ArcFilesDiscriminate.Template + | JsonExportFormat.ISA, ArcFilesDiscriminate.Template -> true + | _ -> false + let jsonFormatDisabled (jf: JsonExportFormat) = + match state.FileType ,jf with + // template does not support isa and rocrate + | ArcFilesDiscriminate.Template, JsonExportFormat.ROCrate + | ArcFilesDiscriminate.Template, JsonExportFormat.ISA -> true + | _ -> false + mainFunctionContainer [ + // modal! + match state.UploadedFile with + | Some af -> + Modals.SelectiveImportModal.Main af model.SpreadsheetModel dispatch (fun _ -> TemplateFromFileState.init() |> setState) + | None -> Html.none + //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. " + ] + ] + Bulma.field.div [ + Bulma.buttons [ + buttons.hasAddons + prop.children [ + JsonExportFormat.ROCrate |> fun jef -> TemplateFromFile.SelectorButton<JsonExportFormat> (jef, state.JsonFormat, setJsonFormat, jsonFormatDisabled jef) + JsonExportFormat.ISA |> fun jef -> TemplateFromFile.SelectorButton<JsonExportFormat> (jef, state.JsonFormat, setJsonFormat, jsonFormatDisabled jef) + JsonExportFormat.ARCtrl |> fun jef -> TemplateFromFile.SelectorButton<JsonExportFormat> (jef, state.JsonFormat, setJsonFormat, jsonFormatDisabled jef) + JsonExportFormat.ARCtrlCompressed |> fun jef -> TemplateFromFile.SelectorButton<JsonExportFormat> (jef, state.JsonFormat, setJsonFormat, jsonFormatDisabled jef) + ] + ] + ] + + Bulma.field.div [ + Bulma.buttons [ + buttons.hasAddons + prop.children [ + ArcFilesDiscriminate.Assay |> fun ft -> TemplateFromFile.SelectorButton<ArcFilesDiscriminate> (ft, state.FileType, setFileType, fileTypeDisabled ft) + ArcFilesDiscriminate.Study |> fun ft -> TemplateFromFile.SelectorButton<ArcFilesDiscriminate> (ft, state.FileType, setFileType, fileTypeDisabled ft) + ArcFilesDiscriminate.Investigation |> fun ft -> TemplateFromFile.SelectorButton<ArcFilesDiscriminate> (ft, state.FileType, setFileType, fileTypeDisabled ft) + ArcFilesDiscriminate.Template |> fun ft -> TemplateFromFile.SelectorButton<ArcFilesDiscriminate> (ft, state.FileType, setFileType, fileTypeDisabled ft) + ] + ] + ] + + Bulma.field.div [ + TemplateFromFile.fileUploadButton(state, setState, dispatch) + ] + ] \ No newline at end of file diff --git a/src/Client/Pages/Settings/SettingsView.fs b/src/Client/Pages/Settings/SettingsView.fs index d60095f4..bea788a8 100644 --- a/src/Client/Pages/Settings/SettingsView.fs +++ b/src/Client/Pages/Settings/SettingsView.fs @@ -119,9 +119,9 @@ let settingsViewComponent (model:Model) dispatch = //Label.label [Label.Props [Style [Color model.SiteStyleState.ColorMode.Accent]]] [str "Advanced Settings"] //customXmlSettings model dispatch - Bulma.label "Advanced Settings" - if model.PageState.IsExpert then - swateCore model dispatch - else - swateExperts model dispatch + //Bulma.label "Advanced Settings" + //if model.PageState.IsExpert then + // swateCore model dispatch + //else + // swateExperts model dispatch ] \ No newline at end of file diff --git a/src/Client/Pages/TemplateMetadata/TemplateMetadata.fs b/src/Client/Pages/TemplateMetadata/TemplateMetadata.fs deleted file mode 100644 index 2e68bab2..00000000 --- a/src/Client/Pages/TemplateMetadata/TemplateMetadata.fs +++ /dev/null @@ -1,59 +0,0 @@ -module TemplateMetadata.Core - -open Fable.React -open Fable.React.Props -open Fable.Core.JsInterop -open Elmish - -open Shared - -open ExcelColors -open Model -open Messages - -open TemplateMetadata - -open TemplateTypes - -let update (msg:Msg) (currentModel: Messages.Model) : Messages.Model * Cmd<Messages.Msg> = - match msg with - | CreateTemplateMetadataWorksheet metadataFieldsOpt -> - let cmd = - Cmd.OfPromise.either - OfficeInterop.TemplateMetadataFunctions.createTemplateMetadataWorksheet - (metadataFieldsOpt) - (curry GenericLog Cmd.none >> DevMsg) - (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd - -open Messages -open Feliz -open Feliz.Bulma - -let defaultMessageEle (model:Model) dispatch = - - mainFunctionContainer [ - Bulma.field.div [ - Bulma.help [ - str "Use this function to create a prewritten template metadata worksheet." - ] - ] - Bulma.field.div [ - Bulma.button.a [ - prop.onClick(fun e -> CreateTemplateMetadataWorksheet Metadata.root |> TemplateMetadataMsg |> dispatch) - Bulma.button.isFullWidth - Bulma.color.isInfo - prop.text "Create metadata" - ] - ] - ] - -let newNameMainElement (model:Messages.Model) dispatch = - Bulma.content [ - - Bulma.label "Template Metadata" - - Bulma.label "Create template metadata worksheet" - - defaultMessageEle model dispatch - ] \ No newline at end of file diff --git a/src/Client/Pages/TermSearch/TermSearchView.fs b/src/Client/Pages/TermSearch/TermSearchView.fs index c8cf860c..e6aeac63 100644 --- a/src/Client/Pages/TermSearch/TermSearchView.fs +++ b/src/Client/Pages/TermSearch/TermSearchView.fs @@ -22,7 +22,7 @@ let update (termSearchMsg: TermSearch.Msg) (currentState:TermSearch.Model) : Ter open Feliz open Feliz.Bulma -open ARCtrl.ISA +open ARCtrl open Fable.Core.JsInterop /// "Fill selected cells with this term" - button // @@ -109,7 +109,7 @@ let Main (model:Messages.Model, dispatch) = mainFunctionContainer [ Bulma.field.div [ - Components.TermSearch.Input(setTerm, dispatch, fullwidth=true, size=Bulma.input.isLarge, ?parent'=model.TermSearchState.ParentTerm, showAdvancedSearch=true) + Components.TermSearch.Input(setTerm, fullwidth=true, size=Bulma.input.isLarge, ?parent=model.TermSearchState.ParentTerm, advancedSearchDispatch=dispatch) ] addButton(model, dispatch) ] diff --git a/src/Client/React.useListener.fs b/src/Client/React.useListener.fs new file mode 100644 index 00000000..5919f490 --- /dev/null +++ b/src/Client/React.useListener.fs @@ -0,0 +1,107 @@ +[<AutoOpenAttribute>] +module ReactHelper + +// https://github.com/Shmew/Feliz.UseListener/blob/master/src/Feliz.UseListener/Listener.fs + +open Browser.Types +open Browser.Dom +open Fable.Core +open Fable.Core.JsInterop +open Fable.Core +open Feliz +open System.ComponentModel + +[<EditorBrowsable(EditorBrowsableState.Never)>] +module Impl = + [<Emit("typeof window !== 'undefined'")>] + let isWindowDefined () : bool = jsNative + + [<Emit("typeof window.addEventListener === 'function'")>] + let isWindowListenerFunction () : bool = jsNative + + [<Emit("Object.defineProperty({}, 'passive', {get () { $0() }})")>] + let definePassive (updater: unit -> unit) : JS.PropertyDescriptor = jsNative + + let allowsPassiveEvents = + let mutable passive = false + + try + if isWindowDefined() && isWindowListenerFunction() then + let options = + jsOptions<AddEventListenerOptions>(fun o -> + o.passive <- true + ) + + window.addEventListener("testPassiveEventSupport", ignore, options) + window.removeEventListener("testPassiveEventSupport", ignore) + with _ -> () + + passive + + let defaultPassive = jsOptions<AddEventListenerOptions>(fun o -> o.passive <- true) + + let adjustPassive (maybeOptions: AddEventListenerOptions option) = + maybeOptions + |> Option.map (fun options -> + if options.passive && not allowsPassiveEvents then + jsOptions<AddEventListenerOptions>(fun o -> + o.capture <- options.capture + o.once <- options.once + o.passive <- false + ) + else options) + + let createRemoveOptions (maybeOptions: AddEventListenerOptions option) = + maybeOptions + |> Option.bind (fun options -> + if options.capture then + Some (jsOptions<RemoveEventListenerOptions>(fun o -> o.capture <- true)) + else None) + +[<Erase;RequireQualifiedAccess>] +module React = + [<Erase>] + type useListener = + static member inline on (eventType: string, action: #Event -> unit, ?options: AddEventListenerOptions) = + let addOptions = React.useMemo((fun () -> Impl.adjustPassive options), [| options |]) + let removeOptions = React.useMemo((fun () -> Impl.createRemoveOptions options), [| options |]) + let fn = React.useMemo((fun () -> unbox<#Event> >> action), [| action |]) + + let listener = React.useCallbackRef(fun () -> + match addOptions with + | Some options -> + document.addEventListener(eventType, fn, options) + | None -> document.addEventListener(eventType, fn) + + React.createDisposable(fun () -> + match removeOptions with + | Some options -> document.removeEventListener(eventType, fn, options) + | None -> document.removeEventListener(eventType, fn) + ) + ) + + React.useEffect(listener) + + [<Erase>] + type useElementListener = + static member inline on (elemRef: IRefValue<#HTMLElement option>, eventType: string, action: #Event -> unit, ?options: AddEventListenerOptions) = + let addOptions = React.useMemo((fun () -> Impl.adjustPassive options), [| options |]) + let removeOptions = React.useMemo((fun () -> Impl.createRemoveOptions options), [| options |]) + let fn = React.useMemo((fun () -> unbox<#Event> >> action), [| action |]) + + let listener = React.useCallbackRef(fun () -> + elemRef.current |> Option.iter(fun elem -> + match addOptions with + | Some options -> elem.addEventListener(eventType, fn, options) + | None -> elem.addEventListener(eventType, fn) + ) + + React.createDisposable(fun () -> + elemRef.current |> Option.iter(fun elem -> + match removeOptions with + | Some options -> elem.removeEventListener(eventType, fn, options) + | None -> elem.removeEventListener(eventType, fn) + )) + ) + + React.useEffect(listener) \ No newline at end of file diff --git a/src/Client/Routing.fs b/src/Client/Routing.fs index 6985fae3..af5375f1 100644 --- a/src/Client/Routing.fs +++ b/src/Client/Routing.fs @@ -14,9 +14,7 @@ type Route = | Info | Protocol | ProtocolSearch -| Dag /// Directed Acylclic Graph | JsonExport -| TemplateMetadata | ActivityLog | Settings | NotFound @@ -28,9 +26,7 @@ type Route = | Route.FilePicker -> "File Picker" | Route.Protocol -> "Templates" | Route.ProtocolSearch -> "Template Search" - | Route.Dag -> "Directed Acylclic Graph" | Route.JsonExport -> "Json Export" - | Route.TemplateMetadata -> "Template Metadata" | Route.Info -> "Info" | Route.ActivityLog -> "Activity Log" | Route.Settings -> "Settings" @@ -38,7 +34,7 @@ type Route = member this.isExpert = match this with - | Route.TemplateMetadata | Route.JsonExport -> true + | Route.JsonExport -> true | _ -> false member this.isActive(currentRoute: Route) = @@ -64,14 +60,10 @@ type Route = createElem [ Html.i [prop.className "fa-solid fa-circle-plus" ];Html.i [prop.className "fa-solid fa-table" ]] p.toStringRdbl | Route.ProtocolSearch -> createElem [ Html.i [prop.className "fa-solid fa-table" ]; Html.i [prop.className "fa-solid fa-magnifying-glass" ]] p.toStringRdbl - | Route.Dag -> - createElem [ Html.i [prop.className "fa-solid fa-diagram-project" ]] p.toStringRdbl | Route.JsonExport -> createElem [ Html.i [prop.className "fa-solid fa-file-export" ]] p.toStringRdbl - | Route.TemplateMetadata -> - createElem [ Html.i [prop.className "fa-solid fa-circle-plus" ];Html.i [prop.className "fa-solid fa-table" ]] p.toStringRdbl | Route.FilePicker -> - createElem [ Html.i [prop.className "fa-solid fa-upload" ]] p.toStringRdbl + createElem [ Html.i [prop.className "fa-solid fa-file-signature" ]] p.toStringRdbl | Route.ActivityLog -> createElem [ Html.i [prop.className "fa-solid fa-timeline" ]] p.toStringRdbl | Route.Info -> @@ -96,9 +88,7 @@ module Routing = map Route.Info (s "Info") map Route.Protocol (s "ProtocolInsert") map Route.ProtocolSearch (s "Protocol" </> s "Search") - map Route.Dag (s "Dag") map Route.JsonExport (s "Experts" </> s "JsonExport") - map Route.TemplateMetadata (s "Experts" </> s "TemplateMetadata") map Route.ActivityLog (s "ActivityLog") map Route.Settings (s "Settings") map Route.NotFound (s "NotFound") diff --git a/src/Client/SharedComponents/ClickOutsideHandler.fs b/src/Client/SharedComponents/ClickOutsideHandler.fs index db8b5743..7f635c1f 100644 --- a/src/Client/SharedComponents/ClickOutsideHandler.fs +++ b/src/Client/SharedComponents/ClickOutsideHandler.fs @@ -1,5 +1,6 @@ namespace Components +open Feliz open Fable.Core open Browser.Types open Fable.Core.JsInterop @@ -8,9 +9,26 @@ type ClickOutsideHandler = static member AddListener(containerId: string, clickedOutsideEvent: Event -> unit) = let rec closeEvent = fun (e: Event) -> + let rmv = fun () -> Browser.Dom.document.removeEventListener("click", closeEvent) let dropdown = Browser.Dom.document.getElementById(containerId) - let isClickedInsideDropdown : bool = dropdown?contains(e.target) - if not isClickedInsideDropdown then - clickedOutsideEvent e - Browser.Dom.document.removeEventListener("click", closeEvent) - Browser.Dom.document.addEventListener("click", closeEvent) \ No newline at end of file + if isNull dropdown then + rmv() + else + let isClickedInsideDropdown : bool = dropdown?contains(e.target) + if not isClickedInsideDropdown then + clickedOutsideEvent e + rmv() + Browser.Dom.document.addEventListener("click", closeEvent) + + static member AddListener(element: IRefValue<HTMLElement option>, clickedOutsideEvent: Event -> unit) = + let rec closeEvent = fun (e: Event) -> + let rmv = fun () -> Browser.Dom.document.removeEventListener("click", closeEvent) + let dropdown = element.current + if dropdown.IsNone then + rmv() + else + let isClickedInsideDropdown : bool = dropdown?contains(e.target) + if not isClickedInsideDropdown then + clickedOutsideEvent e + rmv() + Browser.Dom.document.addEventListener("click", closeEvent) diff --git a/src/Client/SharedComponents/QuickAccessButton.fs b/src/Client/SharedComponents/QuickAccessButton.fs index 3a82add8..471ffe7b 100644 --- a/src/Client/SharedComponents/QuickAccessButton.fs +++ b/src/Client/SharedComponents/QuickAccessButton.fs @@ -30,9 +30,9 @@ type QuickAccessButton = { prop.children [ Bulma.button.a [ prop.tabIndex (if isDisabled then -1 else 0) + prop.className "myNavbarButton" yield! this.ButtonProps prop.disabled isDisabled - prop.className "myNavbarButton" prop.onClick this.Msg prop.children [ Html.div [ diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index 0df72acb..ddaa4b8b 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -3,7 +3,7 @@ open Feliz open Feliz.Bulma open Browser.Types -open ARCtrl.ISA +open ARCtrl open Shared open Fable.Core.JsInterop @@ -28,7 +28,7 @@ module TermSearchAux = let searchByName(query: string, setResults: TermTypes.Term [] -> unit) = async { - let! terms = Api.ontology.searchTerms {|limit = 5; ontologies = []; query=query|} + let! terms = Api.ontology.searchTerms {|limit = 10; ontologies = []; query=query|} setResults terms } @@ -89,6 +89,12 @@ module TermSearchAux = setSearchNameState <| SearchState.init() debouncel debounceStorage "TermSearch" debounceTimer setLoading queryDB () + let dsetter (inp: OntologyAnnotation option, setter, debounceStorage: System.Collections.Generic.Dictionary<string,int>, setLoading: bool -> unit, debounceSetter: int option) = + if debounceSetter.IsSome then + debouncel debounceStorage "SetterDebounce" debounceSetter.Value setLoading setter inp + else + setter inp + module Components = let termSeachNoResults = [ @@ -211,12 +217,34 @@ module TermSearchAux = ] ] ] - + open TermSearchAux open Fable.Core.JsInterop type TermSearch = + static member ToggleSearchContainer (element: ReactElement, ref: IRefValue<HTMLElement option>, searchable: bool, searchableSetter: bool -> unit) = + Bulma.field.div [ + prop.style [style.flexGrow 1; style.position.relative] + prop.ref ref + Bulma.field.hasAddons + prop.children [ + element + Bulma.control.p [ + prop.style [style.marginRight 0] + prop.children [ + Bulma.button.a [ + prop.style [style.borderWidth 0; style.borderRadius 0] + if not searchable then Bulma.color.hasTextGreyLight + Bulma.button.isInverted + prop.onClick(fun _ -> searchableSetter (not searchable)) + prop.children [Bulma.icon [Html.i [prop.className "fa-solid fa-magnifying-glass"]]] + ] + ] + ] + ] + ] + [<ReactComponent>] static member TermSelectItem (term: TermTypes.Term, setTerm, ?isDirectedSearchResult: bool) = let isDirectedSearchResult = defaultArg isDirectedSearchResult false @@ -230,7 +258,8 @@ type TermSearch = ] ] - static member TermSelectArea (id: string, searchNameState: SearchState, searchTreeState: SearchState, setTerm: TermTypes.Term option -> unit, show: bool, width: Styles.ICssUnit, alignRight) = + [<ReactComponent>] + static member TermSelectArea (id: string, searchNameState: SearchState, searchTreeState: SearchState, setTerm: TermTypes.Term option -> unit, show: bool) = let searchesAreComplete = searchNameState.SearchIs = SearchIs.Done && searchTreeState.SearchIs = SearchIs.Done let foundInBoth (term:TermTypes.Term) = (searchTreeState.Results |> Array.contains term) @@ -260,7 +289,7 @@ type TermSearch = Html.div [ prop.id id prop.classes ["term-select-area"; if not show then "is-hidden";] - prop.style [style.width width; if alignRight then style.right 0] + prop.style [style.width (length.perc 100); style.top (length.perc 100)] prop.children [ yield! matchSearchState searchNameState false yield! matchSearchState searchTreeState true @@ -269,27 +298,32 @@ type TermSearch = [<ReactComponent>] static member Input ( - setter: OntologyAnnotation option -> unit, dispatch, - ?input: OntologyAnnotation, ?parent': OntologyAnnotation, - ?showAdvancedSearch: bool, - ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?dropdownWidth: Styles.ICssUnit, ?alignRight: bool, ?displayParent: bool) + setter: OntologyAnnotation option -> unit, + ?input: OntologyAnnotation, ?parent: OntologyAnnotation, + ?debounceSetter: int, ?searchableToggle: bool, + ?advancedSearchDispatch: Messages.Msg -> unit, + ?portalTermSelectArea: HTMLElement, + ?onBlur: Event -> unit, ?onEscape: KeyboardEvent -> unit, ?onEnter: KeyboardEvent -> unit, + ?autofocus: bool, ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?displayParent: bool, ?borderRadius: int, ?border: string, ?minWidth: Styles.ICssUnit) = + let searchableToggle = defaultArg searchableToggle false + let autofocus = defaultArg autofocus false let displayParent = defaultArg displayParent true - let alignRight = defaultArg alignRight false - let dropdownWidth = defaultArg dropdownWidth (length.perc 100) let isExpanded = defaultArg isExpanded false - let showAdvancedSearch = defaultArg showAdvancedSearch false let advancedSearchActive, setAdvancedSearchActive = React.useState(false) let fullwidth = defaultArg fullwidth false let loading, setLoading = React.useState(false) let state, setState = React.useState(input) + let searchable, setSearchable = React.useState(true) let searchNameState, setSearchNameState = React.useState(SearchState.init) let searchTreeState, setSearchTreeState = React.useState(SearchState.init) let isSearching, setIsSearching = React.useState(false) - let debounceStorage, setdebounceStorage = React.useState(newDebounceStorage) - let parent, setParent = React.useState(parent') + let debounceStorage = React.useRef(newDebounceStorage()) + let ref = React.useElementRef() + if onBlur.IsSome then React.useLayoutEffectOnce(fun _ -> ClickOutsideHandler.AddListener (ref, onBlur.Value)) + React.useEffect((fun () -> setState input), dependencies=[|box input|]) let stopSearch() = - debounceStorage.Clear() + debounceStorage.current.Remove("TermSearch") |> ignore setLoading false setIsSearching false setSearchTreeState {searchTreeState with SearchIs = SearchIs.Idle} @@ -299,50 +333,83 @@ type TermSearch = setState oaOpt setter oaOpt setIsSearching false - let startSearch(queryString: string option) = - let oaOpt = queryString |> Option.map (fun s -> OntologyAnnotation.fromString(s) ) - setter oaOpt - setState oaOpt + let startSearch() = + setLoading true setSearchNameState <| SearchState.init() setSearchTreeState <| SearchState.init() setIsSearching true - React.useEffect((fun () -> setParent parent'), dependencies=[|box parent'|]) // careful, check console. might result in maximum dependency depth error. + let registerChange(queryString: string option) = + let oaOpt = queryString |> Option.map (fun s -> OntologyAnnotation(s) ) + dsetter(oaOpt,setter,debounceStorage.current,setLoading,debounceSetter) + setState oaOpt Bulma.control.div [ if isExpanded then Bulma.control.isExpanded if size.IsSome then size.Value - Bulma.control.hasIconsLeft + if not searchableToggle then Bulma.control.hasIconsLeft Bulma.control.hasIconsRight + if not searchableToggle then prop.ref ref prop.style [ if fullwidth then style.flexGrow 1; + if minWidth.IsSome then style.minWidth minWidth.Value ] if loading then Bulma.control.isLoading prop.children [ Bulma.input.text [ + prop.autoFocus autofocus + prop.style [ + if borderRadius.IsSome then style.borderRadius borderRadius.Value + if border.IsSome then style.custom("border", border.Value) + ] if size.IsSome then size.Value if state.IsSome then prop.valueOrDefault state.Value.NameText + prop.onMouseDown(fun e -> e.stopPropagation()) prop.onDoubleClick(fun e -> let s : string = e.target?value if s.Trim() = "" && parent.IsSome && parent.Value.TermAccessionShort <> "" then // trigger get all by parent search - startSearch(None) - allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage, 0) + if searchable then + startSearch() + allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) elif s.Trim() <> "" then - startSearch (Some s) - mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage, 0) + if searchable then + startSearch () + mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) else () ) prop.onChange(fun (s: string) -> if s.Trim() = "" then - startSearch(None) - stopSearch() + registerChange(None) + stopSearch() // When deleting text this should stop search from completing else - startSearch (Some s) - mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage, 1000) + registerChange(Some s) + if searchable then + startSearch() + mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) + ) + prop.onKeyDown(fun e -> + e.stopPropagation() + match e.which with + | 27. -> //escape + if onEscape.IsSome then onEscape.Value e + stopSearch() + | 13. -> //enter + if onEnter.IsSome then onEnter.Value e + | 9. -> //tab + if searchableToggle then + e.preventDefault() + setSearchable (not searchable) + | _ -> () + ) - prop.onKeyDown(key.escape, fun _ -> stopSearch()) ] - TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching, dropdownWidth, alignRight) - Components.searchIcon + let TermSelectArea = TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching) + if portalTermSelectArea.IsSome then + ReactDOM.createPortal(TermSelectArea,portalTermSelectArea.Value) + elif ref.current.IsSome then + ReactDOM.createPortal(TermSelectArea,ref.current.Value) + else + TermSelectArea + if not searchableToggle then Components.searchIcon if state.IsSome && state.Value.Name.IsSome && state.Value.TermAccessionNumber.IsSome && not isSearching then Components.verifiedIcon // Optional elements Html.div [ @@ -353,11 +420,11 @@ type TermSearch = Html.span "Parent: " Html.span $"{parent.Value.NameText}, {parent.Value.TermAccessionShort}" ] - if showAdvancedSearch then + if advancedSearchDispatch.IsSome then Components.AdvancedSearch.Main(advancedSearchActive, setAdvancedSearchActive, (fun t -> setAdvancedSearchActive false Some t |> selectTerm), - dispatch + advancedSearchDispatch.Value ) Html.a [ prop.onClick(fun e -> e.preventDefault(); e.stopPropagation(); setAdvancedSearchActive true) @@ -368,3 +435,12 @@ type TermSearch = ] ] ] + |> fun main -> + if searchableToggle then + TermSearch.ToggleSearchContainer(main, ref, searchable, setSearchable) + else + main + + + //static member InputWithSearchToggle() = + \ No newline at end of file diff --git a/src/Client/SidebarComponents/Navbar.fs b/src/Client/SidebarComponents/Navbar.fs index 9ae30c8b..0bc9471f 100644 --- a/src/Client/SidebarComponents/Navbar.fs +++ b/src/Client/SidebarComponents/Navbar.fs @@ -195,6 +195,7 @@ let NavbarComponent (model : Model) (dispatch : Msg -> unit) (sidebarsize: Model span [AriaHidden true] [ ] span [AriaHidden true] [ ] span [AriaHidden true] [ ] + span [AriaHidden true] [ ] ] ] ] @@ -214,6 +215,13 @@ let NavbarComponent (model : Model) (dispatch : Msg -> unit) (sidebarsize: Model Html.i [prop.className "fa-brands fa-twitter"; prop.style [style.color "#1DA1F2"; style.marginLeft 2]] ] ] + Bulma.navbarItem.a [ + prop.onClick (fun e -> + setState {state with BurgerActive = not state.BurgerActive} + UpdatePageState (Some Routing.Route.Info) |> dispatch + ) + prop.text Routing.Route.Info.toStringRdbl + ] Bulma.navbarItem.a [ prop.href Shared.URLs.SwateWiki ; prop.target "_Blank"; diff --git a/src/Client/SidebarComponents/ResponsiveFA.fs b/src/Client/SidebarComponents/ResponsiveFA.fs index 32bd50bb..a14d1faf 100644 --- a/src/Client/SidebarComponents/ResponsiveFA.fs +++ b/src/Client/SidebarComponents/ResponsiveFA.fs @@ -65,18 +65,16 @@ let triggerResponsiveReturnEle id = let responsiveReturnEle id (fa: string) (faToggled: string) = let notTriggeredId = createNonTriggeredId id let triggeredId = createTriggeredId id - div [Style [ - Position PositionOptions.Relative - ]] [ + Bulma.icon [ Html.i [ prop.style [ - style.position.absolute - style.top 0 - style.left 0 - style.display.block - style.custom("transition", "opacity 0.25s, transform 0.25s") - style.opacity 1 - ] + style.position.absolute + //style.top 0 + //style.left 0 + style.display.block + style.custom("transition", "opacity 0.25s, transform 0.25s") + style.opacity 1 + ] prop.id notTriggeredId prop.onTransitionEnd (fun e -> Fable.Core.JS.setTimeout (fun () -> @@ -89,8 +87,8 @@ let responsiveReturnEle id (fa: string) (faToggled: string) = Html.i [ prop.style [ style.position.absolute - style.top 0 - style.left 0 + //style.top 0 + //style.left 0 style.display.block style.custom("transition", "opacity 0.25s, transform 0.25s") style.opacity 0 diff --git a/src/Client/Spreadsheet/Sidebar.Controller.fs b/src/Client/Spreadsheet/BuildingBlocks.Controller.fs similarity index 74% rename from src/Client/Spreadsheet/Sidebar.Controller.fs rename to src/Client/Spreadsheet/BuildingBlocks.Controller.fs index 7d7d9ce2..393ef38d 100644 --- a/src/Client/Spreadsheet/Sidebar.Controller.fs +++ b/src/Client/Spreadsheet/BuildingBlocks.Controller.fs @@ -1,26 +1,15 @@ -module Spreadsheet.Sidebar.Controller +module Spreadsheet.BuildingBlocks.Controller open System.Collections.Generic open Shared.TermTypes open Shared.OfficeInteropTypes open Spreadsheet open Types -open ARCtrl.ISA +open ARCtrl open Shared module SidebarControllerAux = - let rec createNewTableName (ind: int) names = - let name = "NewTable" + string ind - if Seq.contains name names then - createNewTableName (ind+1) names - else - name - - /// Uses current `ActiveTableIndex` to return next `ActiveTableIndex` whenever a new table is added and we want to - /// switch to the new table. - let getNextActiveTableIndex (state: Spreadsheet.Model) = - if state.Tables.TableCount = 0 then 0 else state.ActiveView.TableIndex + 1 /// <summary> /// Uses the first selected columnIndex from `state.SelectedCells` to determine if new column should be inserted or appended. @@ -45,38 +34,6 @@ module SanityChecks = open SidebarControllerAux -/// <summary>This is the basic function to create new Tables from an array of SwateBuildingBlocks</summary> -let addTable (newTable: ArcTable) (state: Spreadsheet.Model) : Spreadsheet.Model = - let tables = state.Tables - // calculate next index - let newIndex = getNextActiveTableIndex state - tables.AddTable(newTable, newIndex) - { state with - ArcFile = state.ArcFile - ActiveView = ActiveView.Table newIndex } - -/// <summary>This function is used to create multiple tables at once.</summary> -let addTables (tables: ArcTable []) (state: Spreadsheet.Model) : Spreadsheet.Model = - let newIndex = getNextActiveTableIndex state - state.Tables.AddTables(tables, newIndex) - { state with - ArcFile = state.ArcFile - ActiveView = ActiveView.Table (newIndex + tables.Length) } - - -/// <summary>Adds the most basic empty Swate table with auto generated name.</summary> -let createTable (usePrevOutput:bool) (state: Spreadsheet.Model) : Spreadsheet.Model = - let tables = state.ArcFile.Value.Tables() - let newName = createNewTableName 0 tables.TableNames - let newTable = ArcTable.init(newName) - if usePrevOutput && (tables.TableCount-1) >= state.ActiveView.TableIndex then - let table = tables.GetTableAt(state.ActiveView.TableIndex) - let output = table.GetOutputColumn() - let newInput = output.Header.TryOutput().Value |> CompositeHeader.Input - newTable.AddColumn(newInput,output.Cells,forceReplace=true) - let nextState = {state with ArcFile = state.ArcFile} - addTable newTable nextState - let addBuildingBlock(newColumn: CompositeColumn) (state: Spreadsheet.Model) : Spreadsheet.Model = let table = state.ActiveTable // add one to last column index OR to selected column index to append one to the right. diff --git a/src/Client/Spreadsheet/Clipboard.Controller.fs b/src/Client/Spreadsheet/Clipboard.Controller.fs index cb37cbef..bc1f0de3 100644 --- a/src/Client/Spreadsheet/Clipboard.Controller.fs +++ b/src/Client/Spreadsheet/Clipboard.Controller.fs @@ -1,56 +1,114 @@ module Spreadsheet.Clipboard.Controller -open ARCtrl.ISA +open Fable.Core +open ARCtrl open Shared -module ClipboardAux = - let setClipboardCell (state: Spreadsheet.Model) (cell: CompositeCell option) = - let nextState = {state with Clipboard = { state.Clipboard with Cell = cell}} - nextState +let copyCell (cell: CompositeCell) : JS.Promise<unit> = + let tab = cell.ToTabStr() + navigator.clipboard.writeText(tab) -open ClipboardAux +let copyCells (cells: CompositeCell []) : JS.Promise<unit> = + let tab = CompositeCell.ToTabTxt cells + navigator.clipboard.writeText(tab) -let copyCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = - let cell = state.ActiveTable.TryGetCellAt(index) - let nextState = {state with Clipboard = { state.Clipboard with Cell = cell}} - nextState +let copyCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise<unit> = + let cell = state.ActiveTable.Values.[index] + copyCell cell -let copySelectedCell (state: Spreadsheet.Model) : Spreadsheet.Model = +let copyCellsByIndex (indices: (int*int) []) (state: Spreadsheet.Model) : JS.Promise<unit> = + let cells = [|for index in indices do yield state.ActiveTable.Values.[index] |] + log cells + copyCells cells + +let copySelectedCell (state: Spreadsheet.Model) : JS.Promise<unit> = /// Array.head is used until multiple cells are supported, should this ever be intended let index = state.SelectedCells |> Set.toArray |> Array.min - copyCell index state + copyCellByIndex index state + +let copySelectedCells (state: Spreadsheet.Model) : JS.Promise<unit> = + /// Array.head is used until multiple cells are supported, should this ever be intended + let indices = state.SelectedCells |> Set.toArray + copyCellsByIndex indices state -let cutCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = - let cell = state.ActiveTable.TryGetCellAt(index) +let cutCellByIndex (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = + let cell = state.ActiveTable.Values.[index] // Remove selected cell value - - let emptyCell = if cell.IsSome then cell.Value.GetEmptyCell() else state.ActiveTable.GetColumn(fst index).GetDefaultEmptyCell() - state.ActiveTable.UpdateCellAt(fst index,snd index, emptyCell) - let nextState = setClipboardCell state cell - nextState + let emptyCell = cell.GetEmptyCell() + state.ActiveTable.UpdateCellAt(fst index,snd index, emptyCell) + copyCell cell |> Promise.start + state + +let 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] + // Remove selected cell value + let emptyCell = cell.GetEmptyCell() + state.ActiveTable.UpdateCellAt(fst index,snd index, emptyCell) + cells.Add(cell) + copyCells (Array.ofSeq cells) |> Promise.start + state let cutSelectedCell (state: Spreadsheet.Model) : Spreadsheet.Model = /// Array.min is used until multiple cells are supported, should this ever be intended let index = state.SelectedCells |> Set.toArray |> Array.min - cutCell index state + cutCellByIndex index state + +let cutSelectedCells (state: Spreadsheet.Model) : Spreadsheet.Model = + /// Array.min is used until multiple cells are supported, should this ever be intended + let indices = state.SelectedCells |> Set.toArray + cutCellsByIndices indices state + +let pasteCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise<Spreadsheet.Model> = + promise { + let! tab = navigator.clipboard.readText() + let header = state.ActiveTable.Headers.[fst index] + let cell = CompositeCell.fromTabTxt tab |> Array.head |> _.ConvertToValidCell(header) + state.ActiveTable.SetCellAt(fst index, snd index, cell) + return state + } -let pasteCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = - match state.Clipboard.Cell with - // Don't update if no cell in saved - | None -> state - | Some c -> - state.ActiveTable.UpdateCellAt(fst index, snd index, c) - state +let pasteCellsByIndexExtend (index: int*int) (state: Spreadsheet.Model) : JS.Promise<Spreadsheet.Model> = + promise { + let! tab = navigator.clipboard.readText() + let header = state.ActiveTable.Headers.[fst index] + let cells = CompositeCell.fromTabTxt tab |> Array.map _.ConvertToValidCell(header) + let columnIndex, rowIndex = fst index, snd index + let indexedCells = cells |> Array.indexed |> Array.map (fun (i,c) -> (columnIndex, rowIndex + i), c) + state.ActiveTable.SetCellsAt indexedCells + return state + } -let pasteSelectedCell (state: Spreadsheet.Model) : Spreadsheet.Model = +let pasteCellIntoSelected (state: Spreadsheet.Model) : JS.Promise<Spreadsheet.Model> = if state.SelectedCells.IsEmpty then - state + promise {return state} else - // TODO: - //let arr = state.SelectedCells |> Set.toArray - //let isOneColumn = - // let c = fst arr.[0] // can just use head of selected cells as all must be same column - // arr |> Array.forall (fun x -> fst x = c) - //if not isOneColumn then failwith "Can only paste cells in one column at a time!" let minIndex = state.SelectedCells |> Set.toArray |> Array.min - pasteCell minIndex state \ No newline at end of file + pasteCellByIndex minIndex state + +let pasteCellsIntoSelected (state: Spreadsheet.Model) : JS.Promise<Spreadsheet.Model> = + if state.SelectedCells.IsEmpty then + promise {return state} + else + log "here" + let columnIndex = state.SelectedCells |> Set.toArray |> Array.minBy fst |> fst + let selectedSingleColumnCells = state.SelectedCells |> Set.filter (fun index -> fst index = columnIndex) + promise { + let! tab = navigator.clipboard.readText() + let header = state.ActiveTable.Headers.[columnIndex] + 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 + 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 + return state + } \ No newline at end of file diff --git a/src/Client/Spreadsheet/IO.fs b/src/Client/Spreadsheet/IO.fs index c191ec0b..85ba5567 100644 --- a/src/Client/Spreadsheet/IO.fs +++ b/src/Client/Spreadsheet/IO.fs @@ -1,50 +1,59 @@ module Spreadsheet.IO -open ARCtrl.ISA -open ARCtrl.ISA.Spreadsheet -open FsSpreadsheet.Exceljs +open ARCtrl +open ARCtrl.Spreadsheet +open FsSpreadsheet.Js open Shared open FsSpreadsheet -let private tryToConvertAssay (fswb: FsWorkbook) = - try - ArcAssay.fromFsWorkbook fswb |> Assay |> Some - with - | _ -> None - -let private tryToConvertStudy (fswb: FsWorkbook) = - try - ArcStudy.fromFsWorkbook fswb |> Study |> Some - with - | _ -> None - -let private tryToConvertInvestigation (fswb: FsWorkbook) = - try - ArcInvestigation.fromFsWorkbook fswb |> Investigation |> Some - with - | _ -> None - -let private tryToConvertTemplate (fswb: FsWorkbook) = - try - ARCtrl.Template.Spreadsheet.Template.fromFsWorkbook fswb |> Template |> Some - with - | _ -> None - -// List of conversion functions -let private converters = [tryToConvertAssay; tryToConvertStudy; tryToConvertInvestigation; tryToConvertTemplate] - -// TODO: Can this be done better? If we want to allow upload of any isa.xlsx file? -let readFromBytes (bytes: byte []) = - // Try each conversion function and return the first successful result - let rec tryConvert (converters: ('a -> 'b option) list) (json: 'a) : 'b = - match converters with - | [] -> failwith "Unable to parse json to supported isa file." - | convert :: rest -> - match convert json with - | Some result -> result - | None -> tryConvert rest json - promise { - let! fswb = FsSpreadsheet.Exceljs.Xlsx.fromBytes bytes - let arcFile = tryConvert converters fswb - return arcFile - } \ No newline at end of file +module Xlsx = + + let readFromBytes (bytes: byte []) = + // Try each conversion function and return the first successful result + promise { + let! fswb = FsSpreadsheet.Js.Xlsx.fromXlsxBytes bytes + let ws = fswb.GetWorksheets() + let arcfile = + match ws with + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcAssay.metaDataSheetName = ws.Name ) -> + ArcAssay.fromFsWorkbook fswb |> Assay + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcStudy.metaDataSheetName = ws.Name ) -> + ArcStudy.fromFsWorkbook fswb |> Study + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcInvestigation.metaDataSheetName = ws.Name ) -> + ArcInvestigation.fromFsWorkbook fswb |> Investigation + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.Template.metaDataSheetName = ws.Name ) -> + ARCtrl.Spreadsheet.Template.fromFsWorkbook fswb |> Template + | _ -> failwith "Unable to identify given file. Missing metadata sheet with correct name." + return arcfile + } + +module Json = + + open ARCtrlHelper + open ARCtrl + open ARCtrl.Json + + let readFromJson (fileType: ArcFilesDiscriminate) (jsonType: JsonExportFormat) (json: string) = + promise { + let arcfile = + match fileType, jsonType with + | ArcFilesDiscriminate.Investigation, JsonExportFormat.ARCtrl -> ArcInvestigation.fromJsonString json |> ArcFiles.Investigation + | ArcFilesDiscriminate.Investigation, JsonExportFormat.ARCtrlCompressed -> ArcInvestigation.fromCompressedJsonString json |> ArcFiles.Investigation + | ArcFilesDiscriminate.Investigation, JsonExportFormat.ISA -> ArcInvestigation.fromISAJsonString json |> ArcFiles.Investigation + | ArcFilesDiscriminate.Investigation, JsonExportFormat.ROCrate -> ArcInvestigation.fromROCrateJsonString json |> ArcFiles.Investigation + + | ArcFilesDiscriminate.Study, JsonExportFormat.ARCtrl -> ArcStudy.fromJsonString json |> fun x -> ArcFiles.Study(x, []) + | ArcFilesDiscriminate.Study, JsonExportFormat.ARCtrlCompressed -> ArcStudy.fromCompressedJsonString json |> fun x -> ArcFiles.Study(x, []) + | ArcFilesDiscriminate.Study, JsonExportFormat.ISA -> ArcStudy.fromISAJsonString json |> ArcFiles.Study + | ArcFilesDiscriminate.Study, JsonExportFormat.ROCrate -> ArcStudy.fromROCrateJsonString json |> ArcFiles.Study + + | ArcFilesDiscriminate.Assay, JsonExportFormat.ARCtrl -> ArcAssay.fromJsonString json |> ArcFiles.Assay + | ArcFilesDiscriminate.Assay, JsonExportFormat.ARCtrlCompressed -> ArcAssay.fromCompressedJsonString json |> ArcFiles.Assay + | ArcFilesDiscriminate.Assay, JsonExportFormat.ISA -> ArcAssay.fromISAJsonString json |> ArcFiles.Assay + | ArcFilesDiscriminate.Assay, JsonExportFormat.ROCrate -> ArcAssay.fromROCrateJsonString json |> ArcFiles.Assay + + | ArcFilesDiscriminate.Template, JsonExportFormat.ARCtrl -> Template.fromJsonString json |> ArcFiles.Template + | ArcFilesDiscriminate.Template, JsonExportFormat.ARCtrlCompressed -> Template.fromCompressedJsonString json |> ArcFiles.Template + | ArcFilesDiscriminate.Template, anyElse -> failwithf "Error. It is not intended to parse Template from %s format." (string anyElse) + return arcfile + } \ No newline at end of file diff --git a/src/Client/Spreadsheet/Table.Controller.fs b/src/Client/Spreadsheet/Table.Controller.fs index 58c4a35d..4fa02a22 100644 --- a/src/Client/Spreadsheet/Table.Controller.fs +++ b/src/Client/Spreadsheet/Table.Controller.fs @@ -5,11 +5,18 @@ open Shared.TermTypes open Shared.OfficeInteropTypes open Spreadsheet open Types -open ARCtrl.ISA +open ARCtrl open Shared module ControllerTableAux = + let rec createNewTableName (ind: int) names = + let name = "NewTable" + string ind + if Seq.contains name names then + createNewTableName (ind+1) names + else + name + let findEarlierTable (tableIndex:int) (tables: ArcTables) = let indices = [ 0 .. tables.TableCount-1 ] let lower = indices |> Seq.tryFindBack (fun k -> k < tableIndex) @@ -25,12 +32,46 @@ module ControllerTableAux = open ControllerTableAux +let switchTable (nextIndex: int) (state: Spreadsheet.Model) : Spreadsheet.Model = + match state.ActiveView with + | ActiveView.Table i when i = nextIndex -> state + | _ -> + { state with + ActiveCell = None + SelectedCells = Set.empty + ActiveView = ActiveView.Table nextIndex } + +/// <summary>This is the basic function to create new Tables from an array of SwateBuildingBlocks</summary> +let addTable (newTable: ArcTable) (state: Spreadsheet.Model) : Spreadsheet.Model = + state.Tables.AddTable(newTable) + switchTable (state.Tables.TableCount - 1) state + + +/// <summary>This function is used to create multiple tables at once.</summary> +let addTables (tables: ArcTable []) (state: Spreadsheet.Model) : Spreadsheet.Model = + state.Tables.AddTables(tables) + switchTable (state.Tables.TableCount - 1) state + + +/// <summary>Adds the most basic empty Swate table with auto generated name.</summary> +let createTable (usePrevOutput:bool) (state: Spreadsheet.Model) : Spreadsheet.Model = + let tables = state.ArcFile.Value.Tables() + let newName = createNewTableName 0 tables.TableNames + let newTable = ArcTable.init(newName) + if usePrevOutput && ((tables.TableCount-1) >= state.ActiveView.TableIndex) then + let table = tables.GetTableAt(state.ActiveView.TableIndex) + let output = table.GetOutputColumn() + let newInput = output.Header.TryOutput().Value |> CompositeHeader.Input + newTable.AddColumn(newInput,output.Cells,forceReplace=true) + let nextState = {state with ArcFile = state.ArcFile} + addTable newTable nextState + let updateTableOrder (prevIndex:int, newIndex:int) (state:Spreadsheet.Model) = state.Tables.MoveTable(prevIndex, newIndex) {state with ArcFile = state.ArcFile} -let resetTableState () : LocalHistory.Model * Spreadsheet.Model = - LocalHistory.Model.init().ResetAll(), +let resetTableState () : Spreadsheet.Model = + LocalHistory.Model.ResetHistoryWebStorage() Spreadsheet.Model.init() let renameTable (tableIndex:int) (newName: string) (state: Spreadsheet.Model) : Spreadsheet.Model = @@ -45,22 +86,18 @@ let removeTable (removeIndex: int) (state: Spreadsheet.Model) : Spreadsheet.Mode // if active table is removed get the next closest table and set it active match state.ActiveView with | ActiveView.Table i when i = removeIndex -> - let nextView = - let neighbors = findNeighborTables removeIndex state.Tables - match neighbors with - | Some (i, _), _ -> ActiveView.Table i - | None, Some (i, _) -> ActiveView.Table i - // This is a fallback option, which should never be hit - | _ -> ActiveView.Metadata - { state with - ArcFile = state.ArcFile - ActiveView = nextView } + let neighbors = findNeighborTables removeIndex state.Tables + match neighbors with + | Some (i, _), _ -> + switchTable i state + | None, Some (i, _) -> + switchTable i state + | _ -> { state with ActiveView = ActiveView.Metadata } | ActiveView.Table i -> // Tables still exist and an inactive one was removed. Just remove it. - let nextTable_Index = if i > removeIndex then i - 1 else i + let nextTableIndex = if i > removeIndex then i - 1 else i { state with - ArcFile = state.ArcFile - ActiveView = ActiveView.Table nextTable_Index } - | _ -> state + ActiveView = ActiveView.Table nextTableIndex } + | _ -> {state with ActiveView = ActiveView.Metadata } ///<summary>Add `n` rows to active table.</summary> let addRows (n: int) (state: Spreadsheet.Model) : Spreadsheet.Model = @@ -86,6 +123,18 @@ let deleteColumn (index: int) (state: Spreadsheet.Model) : Spreadsheet.Model = ArcFile = state.ArcFile SelectedCells = Set.empty} +let setColumn (index: int) (column: CompositeColumn) (state: Spreadsheet.Model) : Spreadsheet.Model = + state.ActiveTable.UpdateColumn (index, column.Header, column.Cells) + {state with + ArcFile = state.ArcFile + SelectedCells = Set.empty} + +let moveColumn (current: int) (next: int) (state: Spreadsheet.Model) : Spreadsheet.Model = + state.ActiveTable.MoveColumn (current, next) + {state with + ArcFile = state.ArcFile + SelectedCells = Set.empty } + let fillColumnWithCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = let cell = state.ActiveTable.TryGetCellAt index let columnIndex = fst index @@ -93,10 +142,43 @@ let fillColumnWithCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet let cell = cell|> Option.defaultValue (column.GetDefaultEmptyCell()) if i = columnIndex then for cellRowIndex in 0 .. column.Cells.Length-1 do + let cell = cell state.ActiveTable.UpdateCellAt(columnIndex, cellRowIndex, cell) ) {state with ArcFile = state.ArcFile} +/// <summary> +/// Transform cells of given indices to their empty equivalents +/// </summary> +/// <param name="indexArr"></param> +/// <param name="state"></param> +let clearCells (indexArr: (int*int) []) (state: Spreadsheet.Model) : Spreadsheet.Model = + let table = state.ActiveTable + let newCells = [| + for index in indexArr do + let cell = table.Values.[index] + let emptyCell = cell.GetEmptyCell() + index, emptyCell + |] + table.SetCellsAt newCells + state + +open Fable.Core +open System + +let selectRelativeCell (index: int*int) (move: int*int) (table: ArcTable) = + //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) + //if rowIndex = -1 then + // U2.Case2 columnIndex + //else + // U2.Case1 (columnIndex, rowIndex) + columnIndex, rowIndex + // Ui depends on main column name, maybe change this to depends on BuildingBlockType? // Header main column name must be updated diff --git a/src/Client/States/ARCitect.fs b/src/Client/States/ARCitect.fs index 54314cae..759b757b 100644 --- a/src/Client/States/ARCitect.fs +++ b/src/Client/States/ARCitect.fs @@ -1,16 +1,19 @@ module Model.ARCitect -open ARCtrl.ISA +open ARCtrl type Msg = | Init | Error of exn + | RequestPaths of selectDirectories: bool | AssayToARCitect of ArcAssay | StudyToARCitect of ArcStudy - | TriggerSwateClose + | InvestigationToARCitect of ArcInvestigation type IEventHandler = { Error: exn -> unit - AssayToSwate: {| ArcAssayJsonString: string |} -> unit - StudyToSwate: {| ArcStudyJsonString: string |} -> unit + AssayToSwate : {| ArcAssayJsonString: string |} -> unit + StudyToSwate : {| ArcStudyJsonString: string |} -> unit + InvestigationToSwate : {| ArcInvestigationJsonString: string |} -> unit + PathsToSwate : {| paths: string [] |} -> unit } \ No newline at end of file diff --git a/src/Client/States/DagState.fs b/src/Client/States/DagState.fs deleted file mode 100644 index f348d80b..00000000 --- a/src/Client/States/DagState.fs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Dag - -open Shared.OfficeInteropTypes - -type HtmlString = string - -type Model = { - Loading : bool - DagHtml : HtmlString option -} with - static member init() = { - Loading = false - DagHtml = None - } - -type Msg = -//Client -| UpdateLoading of bool -// -| ParseTablesOfficeInteropRequest -| ParseTablesDagServerRequest of (string * BuildingBlock []) [] -| ParseTablesDagServerResponse of dagHtml:string \ No newline at end of file diff --git a/src/Client/States/JsonExporterState.fs b/src/Client/States/JsonExporterState.fs deleted file mode 100644 index 2b474dfa..00000000 --- a/src/Client/States/JsonExporterState.fs +++ /dev/null @@ -1,57 +0,0 @@ -module Model.JsonExporter - -open Shared -open Shared.OfficeInteropTypes - -type Model = { - /// Use this value to determine on click which export value to use - CurrentExportType : JsonExportType option - // - TableJsonExportType : JsonExportType - WorkbookJsonExportType : JsonExportType - XLSXParsingExportType : JsonExportType - Loading : bool - ShowTableExportTypeDropdown : bool - ShowWorkbookExportTypeDropdown : bool - ShowXLSXExportTypeDropdown : bool - // XLSX upload with json parsing - XLSXByteArray : byte [] -} with - static member init() = { - - CurrentExportType = None - // - TableJsonExportType = JsonExportType.Assay - WorkbookJsonExportType = JsonExportType.Assay - XLSXParsingExportType = JsonExportType.Assay - Loading = false - ShowTableExportTypeDropdown = false - ShowWorkbookExportTypeDropdown = false - ShowXLSXExportTypeDropdown = false - - // XLSX upload with json parsing - XLSXByteArray = Array.empty - } - -type Msg = -// Style -| UpdateLoading of bool -| UpdateShowTableExportTypeDropdown of bool -| UpdateShowWorkbookExportTypeDropdown of bool -| UpdateShowXLSXExportTypeDropdown of bool -| CloseAllDropdowns -| UpdateTableJsonExportType of JsonExportType -| UpdateWorkbookJsonExportType of JsonExportType -| UpdateXLSXParsingExportType of JsonExportType -// -| ParseTableOfficeInteropRequest -/// parse active annotation table to building blocks -| ParseTableServerRequest of worksheetName:string * BuildingBlock [] -| ParseTableServerResponse of string -/// Parse all annotation tables to buildingblocks -| ParseTablesOfficeInteropRequest -| ParseTablesServerRequest of (string * BuildingBlock []) [] -// XLSX upload with json parsing -| StoreXLSXByteArray of byte [] -| ParseXLSXToJsonRequest of byte [] -| ParseXLSXToJsonResponse of string \ No newline at end of file diff --git a/src/Client/States/LocalHistory.fs b/src/Client/States/LocalHistory.fs index d556a493..9e0ca45e 100644 --- a/src/Client/States/LocalHistory.fs +++ b/src/Client/States/LocalHistory.fs @@ -52,9 +52,8 @@ module HistoryOrder = // member this.toJson() = Json.serialize this module ConversionTypes = - open ARCtrl.ISA - open ARCtrl.ISA.Json - open ARCtrl.Template.Json + open ARCtrl + open ARCtrl.Json open Shared [<RequireQualifiedAccess>] @@ -73,9 +72,9 @@ module ConversionTypes = static member fromSpreadsheetModel (model: Spreadsheet.Model) = let jsonArcFile, jsonString = match model.ArcFile with - | Some (ArcFiles.Investigation i) -> JsonArcFiles.Investigation, i.ToArcJsonString() - | Some (ArcFiles.Study (s,al)) -> JsonArcFiles.Study, s.ToArcJsonString() - | Some (ArcFiles.Assay a) -> JsonArcFiles.Assay, a.ToArcJsonString() + | Some (ArcFiles.Investigation i) -> JsonArcFiles.Investigation, ArcInvestigation.toCompressedJsonString 0 i + | Some (ArcFiles.Study (s,al)) -> JsonArcFiles.Study, ArcStudy.toCompressedJsonString 0 s + | Some (ArcFiles.Assay a) -> JsonArcFiles.Assay, ArcAssay.toCompressedJsonString 0 a | Some (ArcFiles.Template t) -> JsonArcFiles.Template, Template.toJsonString 0 t | None -> JsonArcFiles.None, "" { @@ -85,18 +84,21 @@ module ConversionTypes = } member this.ToSpreadsheetModel() = let init = Spreadsheet.Model.init() - let arcFile = - match this.JsonArcFiles with - | JsonArcFiles.Investigation -> ArcInvestigation.fromArcJsonString this.JsonString |> ArcFiles.Investigation |> Some - | JsonArcFiles.Study -> ArcStudy.fromArcJsonString this.JsonString |> fun s -> ArcFiles.Study(s, []) |> Some - | JsonArcFiles.Assay -> ArcAssay.fromArcJsonString this.JsonString |> ArcFiles.Assay |> Some - | JsonArcFiles.Template -> Template.fromJsonString this.JsonString |> ArcFiles.Template |> Some - | JsonArcFiles.None -> None - { - init with - ActiveView = this.ActiveView - ArcFile = arcFile - } + try + 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.Assay -> ArcAssay.fromCompressedJsonString this.JsonString |> ArcFiles.Assay |> Some + | JsonArcFiles.Template -> Template.fromJsonString this.JsonString |> ArcFiles.Template |> Some + | JsonArcFiles.None -> None + { + init with + ActiveView = this.ActiveView + ArcFile = arcFile + } + with + | _ -> init static member toSpreadsheetModel (sessionStorage: SessionStorage) = sessionStorage.ToSpreadsheetModel() @@ -197,7 +199,6 @@ type Model = // if e.g at position 4 and we create new table state from position 4 we want to delete position 0 .. 3 and use 4 as new 0 let rebranchedList, toRemoveList1 = if this.HistoryCurrentPosition <> 0 then - printfn "[HISTORY] Rebranch to %i" this.HistoryCurrentPosition this.HistoryOrder |> List.splitAt this.HistoryCurrentPosition |> fun (remove, keep) -> keep, remove @@ -220,10 +221,12 @@ type Model = Browser.WebStorage.sessionStorage.setItem(Keys.swate_session_history_key, HistoryOrder.toJson(nextState.HistoryOrder)) // reset new table position to 0 Browser.WebStorage.sessionStorage.setItem(Keys.swate_session_history_position, "0") - printfn "[HISTORY] length: %i" nextState.HistoryOrder.Length nextState - member this.ResetAll() = - Browser.WebStorage.localStorage.clear() + static member ResetHistoryWebStorage() = + Browser.WebStorage.localStorage.removeItem(Keys.swate_local_spreadsheet_key) Browser.WebStorage.sessionStorage.clear() + + member this.ResetAll() = + Model.ResetHistoryWebStorage() Model.init() \ No newline at end of file diff --git a/src/Client/OfficeInterop/OfficeInteropState.fs b/src/Client/States/OfficeInteropState.fs similarity index 98% rename from src/Client/OfficeInterop/OfficeInteropState.fs rename to src/Client/States/OfficeInteropState.fs index c3c94b49..d828ce2d 100644 --- a/src/Client/OfficeInterop/OfficeInteropState.fs +++ b/src/Client/States/OfficeInteropState.fs @@ -29,6 +29,7 @@ type Msg = // create and update table element functions | CreateAnnotationTable of tryUsePrevOutput:bool | AnnotationtableCreated + | TryFindAnnotationTable | AnnotationTableExists of TryFindAnnoTableResult | InsertOntologyTerm of TermMinimal | AddAnnotationBlock of InsertBuildingBlock diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index 6eabcc64..bb802a62 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -2,14 +2,18 @@ namespace Spreadsheet open Shared open OfficeInteropTypes -open ARCtrl.ISA +open ARCtrl +open Fable.Core + +type ColumnType = +| Main +| Unit +| TSR +| TAN +with + member this.IsMainColumn = match this with | Main -> true | _ -> false + member this.IsRefColumn = not this.IsMainColumn -type TableClipboard = { - Cell: CompositeCell option -} with - static member init() = { - Cell = None - } [<RequireQualifiedAccess>] type ActiveView = @@ -29,15 +33,30 @@ with type Model = { ActiveView: ActiveView SelectedCells: Set<int*int> + ActiveCell: (U2<int,(int*int)> * ColumnType) option ArcFile: ArcFiles option - Clipboard: TableClipboard } with + member this.CellIsActive(index: U2<int, int*int>, columnType) = + match this.ActiveCell, index with + | Some (U2.Case1 (headerIndex), ct), U2.Case1 (targetIndex) -> headerIndex = targetIndex && ct = columnType + | Some (U2.Case2 (ci, ri), ct), U2.Case2 targetIndex -> (ci,ri) = targetIndex && ct = columnType + | _ -> false + member this.CellIsIdle(index: U2<int, int*int>, columnType) = + this.CellIsActive(index, columnType) |> not static member init() = { ActiveView = ActiveView.Metadata SelectedCells = Set.empty + ActiveCell = None ArcFile = None - Clipboard = TableClipboard.init() + } + + static member init(arcFile: ArcFiles) = + { + ActiveView = ActiveView.Metadata + SelectedCells = Set.empty + ActiveCell = None + ArcFile = Some arcFile } member this.Tables with get() = @@ -63,13 +82,36 @@ type Model = { match this.ArcFile with | Some (Assay a) -> a | _ -> ArcAssay.init("ASSAY_NULL") member this.headerIsSelected = not this.SelectedCells.IsEmpty && this.SelectedCells |> Seq.exists (fun (c,r) -> r = 0) + member this.CanHaveTables() = + match this.ArcFile with + | Some (ArcFiles.Assay _) | Some (ArcFiles.Study _) -> true + | _ -> false + member this.TableViewIsActive() = + match this.ActiveView with + | ActiveView.Table i -> true + | _ -> false + +[<RequireQualifiedAccess>] +type Key = + | Up + | Down + | Left + | Right + type Msg = // <--> UI <--> +| UpdateState of Model | UpdateCell of (int*int) * CompositeCell +| UpdateCells of ((int*int) * CompositeCell) [] | UpdateHeader of columIndex: int * CompositeHeader | UpdateActiveView of ActiveView | UpdateSelectedCells of Set<int*int> +| MoveSelectedCell of Key +| MoveColumn of current:int * next:int +| UpdateActiveCell of (U2<int,(int*int)> * ColumnType) option +| SetActiveCellFromSelected +| AddTable of ArcTable | RemoveTable of index:int | RenameTable of index:int * name:string | UpdateTableOrder of pre_index:int * new_index:int @@ -78,18 +120,27 @@ type Msg = | DeleteRow of int | DeleteRows of int [] | DeleteColumn of int +| SetColumn of index:int * column: CompositeColumn | CopySelectedCell +| CopySelectedCells | CutSelectedCell +| CutSelectedCells | PasteSelectedCell +| PasteSelectedCells | CopyCell of index:(int*int) +| CopyCells of indices:(int*int) [] | CutCell of index:(int*int) | PasteCell of index:(int*int) +/// This Msg will paste all cell from clipboard into column starting from index. It will extend the table if necessary. +| PasteCellsExtend of index:(int*int) +| Clear of index:(int*int) [] +| ClearSelected | FillColumnWithTerm of index:(int*int) // /// Update column of index to new column type defined by given SwateCell.emptyXXX // | EditColumn of index: int * newType: SwateCell * b_type: BuildingBlockType option /// This will reset Spreadsheet.Model to Spreadsheet.Model.init() and clear all webstorage. | Reset -| SetArcFileFromBytes of byte [] +| ImportXlsx of byte [] // <--> INTEROP <--> | CreateAnnotationTable of tryUsePrevOutput:bool | AddAnnotationBlock of CompositeColumn @@ -102,14 +153,10 @@ type Msg = | UpdateTermColumns | UpdateTermColumnsResponse of TermTypes.TermSearchable [] /// Starts chain to export active table to isa json -| ExportJsonTable -/// Starts chain to export all tables to isa json -| ExportJsonTables +| ExportJson of ArcFiles * JsonExportFormat /// Starts chain to export all tables to xlsx swate tables. | ExportXlsx of ArcFiles | ExportXlsxDownload of filename: string * byte [] -/// Starts chain to parse all tables to DAG -| ParseTablesToDag // <--> Result Messages <--> ///// This message will save `Model` to local storage and to session storage for history //| Success of Model diff --git a/src/Client/States/SpreadsheetInterface.fs b/src/Client/States/SpreadsheetInterface.fs index 8fb498df..bed6b720 100644 --- a/src/Client/States/SpreadsheetInterface.fs +++ b/src/Client/States/SpreadsheetInterface.fs @@ -3,27 +3,26 @@ namespace SpreadsheetInterface open Shared open OfficeInteropTypes -open ARCtrl.ISA +open ARCtrl ///<summary>This type is used to interface between standalone, electron and excel logic and will forward the command to the correct logic.</summary> type Msg = | Initialize of Swatehost | CreateAnnotationTable of tryUsePrevOutput:bool | RemoveBuildingBlock +| AddTable of ArcTable | AddAnnotationBlock of CompositeColumn | AddAnnotationBlocks of CompositeColumn [] -| JoinTable of ArcTable * index: int option * options: TableJoinOptions option -| ImportFile of ArcFiles +/// This function will do preprocessing on the table to join +| JoinTable of ArcTable * columnIndex: int option * options: TableJoinOptions option +| UpdateArcFile of ArcFiles /// Open modal for selected building block, allows editing on standalone only. | EditBuildingBlock /// Inserts TermMinimal to selected fields of one column | InsertOntologyAnnotation of OntologyAnnotation | InsertFileNames of string list +| ImportXlsx of byte [] /// Starts chain to export active table to isa json -| ExportJsonTable -/// Starts chain to export all tables to isa json -| ExportJsonTables -/// Starts chain to parse all tables to DAG -| ParseTablesToDag +| ExportJson of ArcFiles * JsonExportFormat | UpdateTermColumns | UpdateTermColumnsResponse of TermTypes.TermSearchable [] \ No newline at end of file diff --git a/src/Client/States/TemplateMetadataState.fs b/src/Client/States/TemplateMetadataState.fs deleted file mode 100644 index 78e52cd8..00000000 --- a/src/Client/States/TemplateMetadataState.fs +++ /dev/null @@ -1,18 +0,0 @@ -namespace TemplateMetadata - -open Shared -open TemplateTypes.Metadata - -type Model = { - Default: obj - MetadataFields : MetadataField option -} with - static member init() = { - Default = "" - MetadataFields = None - } - -type Msg = -| CreateTemplateMetadataWorksheet of MetadataField -//| GetTemplateMetadataJsonSchemaRequest -//| GetTemplateMetadataJsonSchemaResponse of string \ No newline at end of file diff --git a/src/Client/Update.fs b/src/Client/Update.fs index 7f614aeb..c5efd4fc 100644 --- a/src/Client/Update.fs +++ b/src/Client/Update.fs @@ -104,267 +104,9 @@ module Dev = (curry GenericError Cmd.none >> DevMsg) currentState, cmd -let handleApiRequestMsg (reqMsg: ApiRequestMsg) (currentState: ApiState) : ApiState * Cmd<Messages.Msg> = - - let handleTermSuggestionRequest (apiFunctionname:string) (responseHandler: Term [] -> ApiMsg) queryString = - let currentCall = { - FunctionName = apiFunctionname - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - let nextCmd = - Cmd.OfAsync.either - Api.api.getTermSuggestions - {|n= 5; query = queryString; ontology = None|} - (responseHandler >> Api) - (ApiError >> Api) - - nextState,nextCmd - - let handleUnitTermSuggestionRequest (apiFunctionname:string) (responseHandler: (Term []) -> ApiMsg) queryString = - let currentCall = { - FunctionName = apiFunctionname - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - let nextCmd = - Cmd.OfAsync.either - Api.api.getUnitTermSuggestions - {|n= 5; query = queryString|} - (responseHandler >> Api) - (ApiError >> Api) - - nextState,nextCmd - - let handleTermSuggestionByParentTermRequest (apiFunctionname:string) (responseHandler: Term [] -> ApiMsg) queryString (parent:TermMinimal) = - let currentCall = { - FunctionName = apiFunctionname - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - let nextCmd = - Cmd.OfAsync.either - Api.api.getTermSuggestionsByParentTerm - {|n= 5; query = queryString; parent_term = parent|} - (responseHandler >> Api) - (ApiError >> Api) - - nextState,nextCmd - - match reqMsg with - - | GetNewUnitTermSuggestions (queryString) -> - handleUnitTermSuggestionRequest - "getUnitTermSuggestions" - (UnitTermSuggestionResponse >> Response) - queryString - - | FetchAllOntologies -> - let currentCall = { - FunctionName = "getAllOntologies" - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - - nextState, - Cmd.OfAsync.either - Api.api.getAllOntologies - () - (FetchAllOntologiesResponse >> Response >> Api) - (ApiError >> Api) - - | SearchForInsertTermsRequest (tableTerms) -> - let currentCall = { - FunctionName = "getTermsByNames" - Status = Pending - } - let nextState = { - currentState with - currentCall = currentCall - } - let cmd = - Cmd.OfAsync.either - Api.api.getTermsByNames - tableTerms - (SearchForInsertTermsResponse >> Response >> Api) - (fun e -> - Msg.Batch [ - OfficeInterop.UpdateFillHiddenColsState OfficeInterop.FillHiddenColsState.Inactive |> OfficeInteropMsg - ApiError e |> Api - ] ) - let stateCmd = OfficeInterop.UpdateFillHiddenColsState OfficeInterop.FillHiddenColsState.ServerSearchDatabase |> OfficeInteropMsg |> Cmd.ofMsg - let cmds = Cmd.batch [cmd; stateCmd] - nextState, cmds - // - | GetAppVersion -> - let currentCall = { - FunctionName = "getAppVersion" - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - - let cmd = - Cmd.OfAsync.either - Api.serviceApi.getAppVersion - () - (GetAppVersionResponse >> Response >> Api) - (ApiError >> Api) - - nextState, cmd - - -let handleApiResponseMsg (resMsg: ApiResponseMsg) (currentState: ApiState) : ApiState * Cmd<Messages.Msg> = - - let handleTermSuggestionResponse (responseHandler: Term [] -> Msg) (suggestions: Term[]) = - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - - let cmds = Cmd.batch [ - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - suggestions |> responseHandler |> Cmd.ofMsg - ] - - nextState, cmds - - let handleUnitTermSuggestionResponse (responseHandler: Term [] -> Msg) (suggestions: Term[]) = - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - - let cmds = Cmd.batch [ - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - (suggestions) |> responseHandler |> Cmd.ofMsg - ] - - nextState, cmds - - match resMsg with - | UnitTermSuggestionResponse (suggestions) -> - - handleUnitTermSuggestionResponse - (BuildingBlock.Msg.NewUnitTermSuggestions >> BuildingBlockMsg) - suggestions - - | FetchAllOntologiesResponse onts -> - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - - let cmds = Cmd.batch [ - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - onts |> NewSearchableOntologies |> PersistentStorage |> Cmd.ofMsg - ] - - nextState, cmds - - | SearchForInsertTermsResponse (termsWithSearchResult) -> - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - let cmd = - SpreadsheetInterface.UpdateTermColumnsResponse termsWithSearchResult |> InterfaceMsg |> Cmd.ofMsg - let loggingCmd = - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - nextState, Cmd.batch [cmd; loggingCmd] - - // - | GetAppVersionResponse appVersion -> - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - - let cmds = Cmd.batch [ - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - appVersion |> UpdateAppVersion |> PersistentStorage |> Cmd.ofMsg - ] - - nextState, cmds - -open Dev -open Messages - -let handleApiMsg (apiMsg:ApiMsg) (currentState:ApiState) : ApiState * Cmd<Messages.Msg> = - match apiMsg with - | ApiError e -> - - let failedCall = { - currentState.currentCall with - Status = Failed (e.GetPropagatedError()) - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = failedCall::currentState.callHistory - } - let batch = Cmd.batch [ - let modalName = "GenericError" - Cmd.ofEffect(fun _ -> Modals.Controller.renderModal(modalName, Modals.ErrorModal.errorModal(e))) - curry GenericLog Cmd.none ("Error",sprintf "[ApiError]: Call %s failed with: %s" failedCall.FunctionName (e.GetPropagatedError())) |> DevMsg |> Cmd.ofMsg - ] - - nextState, batch - - | ApiSuccess (level,logMsg) -> - currentState, curry GenericLog Cmd.none (level,logMsg) |> DevMsg |> Cmd.ofMsg - - | Request req -> - handleApiRequestMsg req currentState - | Response res -> - handleApiResponseMsg res currentState - -let handlePersistenStorageMsg (persistentStorageMsg: PersistentStorageMsg) (currentState:PersistentStorageState) : PersistentStorageState * Cmd<Msg> = +let handlePersistenStorageMsg (persistentStorageMsg: PersistentStorage.Msg) (currentState:PersistentStorageState) : PersistentStorageState * Cmd<Msg> = match persistentStorageMsg with - | NewSearchableOntologies onts -> + | PersistentStorage.NewSearchableOntologies onts -> let nextState = { currentState with SearchableOntologies = onts |> Array.map (fun ont -> ont.Name |> SorensenDice.createBigrams, ont) @@ -372,12 +114,14 @@ let handlePersistenStorageMsg (persistentStorageMsg: PersistentStorageMsg) (curr } nextState,Cmd.none - | UpdateAppVersion appVersion -> + | PersistentStorage.UpdateAppVersion appVersion -> let nextState = { currentState with AppVersion = appVersion } nextState,Cmd.none + | PersistentStorage.UpdateShowSidebar show -> + {currentState with ShowSideBar = show}, Cmd.none let handleBuildingBlockDetailsMsg (topLevelMsg:BuildingBlockDetailsMsg) (currentState: BuildingBlockDetailsState) : BuildingBlockDetailsState * Cmd<Msg> = match topLevelMsg with @@ -421,19 +165,18 @@ let handleBuildingBlockDetailsMsg (topLevelMsg:BuildingBlockDetailsMsg) (current Modals.Controller.renderModal("BuildingBlockDetails", Modals.BuildingBlockDetailsModal.buildingBlockDetailModal(nextState, dispatch)) ) nextState, cmd - -let handleTopLevelMsg (topLevelMsg:TopLevelMsg) (currentModel: Model) : Model * Cmd<Msg> = - match topLevelMsg with - // Client - | CloseSuggestions -> - let nextModel = { - currentModel with - AddBuildingBlockState = { - currentModel.AddBuildingBlockState with - ShowUnit2TermSuggestions = false - } - } - nextModel, Cmd.none + +module Ontologies = + let update (omsg: Ontologies.Msg) (model: Model) = + match omsg with + | Ontologies.GetOntologies -> + let cmd = + Cmd.OfAsync.either + Api.api.getAllOntologies + () + (PersistentStorage.NewSearchableOntologies >> PersistentStorageMsg) + (curry GenericError Cmd.none >> DevMsg) + model, cmd let update (msg : Msg) (model : Model) : Model * Cmd<Msg> = let innerUpdate (msg: Msg) (currentModel: Model) = @@ -496,28 +239,10 @@ let update (msg : Msg) (model : Model) : Model * Cmd<Msg> = // https://stackoverflow.com/questions/42642863/office-js-nullifies-browser-history-functions-breaking-history-usage //| Navigate route -> // currentModel, Navigation.newUrl (Routing.Route.toRouteUrl route) - | Bounce (delay, bounceId, msgToBounce) -> - let (debouncerModel, debouncerCmd) = - currentModel.DebouncerState - |> Debouncer.bounce delay bounceId msgToBounce - - let nextModel = { - currentModel with - DebouncerState = debouncerModel - } - - nextModel,Cmd.map DebouncerSelfMsg debouncerCmd - - | DebouncerSelfMsg debouncerMsg -> - let nextDebouncerState, debouncerCmd = - Debouncer.update debouncerMsg currentModel.DebouncerState - - let nextModel = { - currentModel with - DebouncerState = nextDebouncerState - } - nextModel, debouncerCmd + | OntologyMsg msg -> + let nextModel, cmd = Ontologies.update msg model + nextModel, cmd | OfficeInteropMsg excelMsg -> let nextModel,nextCmd = Update.OfficeInterop.update currentModel excelMsg @@ -555,16 +280,7 @@ let update (msg : Msg) (model : Model) : Model * Cmd<Msg> = } nextModel,nextCmd - | Api apiMsg -> - let nextApiState,nextCmd = currentModel.ApiState |> handleApiMsg apiMsg - - let nextModel = { - currentModel with - ApiState = nextApiState - } - nextModel,nextCmd - - | PersistentStorage persistentStorageMsg -> + | PersistentStorageMsg persistentStorageMsg -> let nextPersistentStorageState,nextCmd = currentModel.PersistentStorageState |> handlePersistenStorageMsg persistentStorageMsg @@ -639,29 +355,11 @@ let update (msg : Msg) (model : Model) : Model * Cmd<Msg> = CytoscapeModel = nextState} nextModel, nextCmd - | JsonExporterMsg msg -> - let nextModel, nextCmd = currentModel |> JsonExporter.Core.update msg - nextModel, nextCmd - - | TemplateMetadataMsg msg -> - let nextModel, nextCmd = currentModel |> TemplateMetadata.Core.update msg - nextModel, nextCmd - - | DagMsg msg -> - let nextModel, nextCmd = currentModel |> Dag.Core.update msg - nextModel, nextCmd - - | TopLevelMsg msg -> - let nextModel, nextCmd = - handleTopLevelMsg msg currentModel - - nextModel, nextCmd - /// This function is used to determine which msg should be logged to activity log. /// The function is exception based, so msg which should not be logged needs to be added here. let matchMsgToLog (msg: Msg) = match msg with - | Bounce _ | DevMsg _ | UpdatePageState _ -> false + | DevMsg _ | UpdatePageState _ -> false | _ -> true let logg (msg:Msg) (model: Model) : Model = diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index 7e07c787..3caf4601 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -12,11 +12,46 @@ open Elmish open Model open Shared open Fable.Core.JsInterop +open Shared.ARCtrlHelper -module private Helper = +/// This seems like such a hack :( +module private ExcelHelper = + + open Fable.Core open ExcelJS.Fable.GlobalBindings - let initializeAddIn () = Office.onReady() + let initializeAddIn () = Office.onReady().``then``(fun _ -> ()) |> Async.AwaitPromise + + /// Office-js will kill iframe loading in ARCitect, therefore we must load it conditionally + let addOfficeJsScript(callback: unit -> unit) = + let cdn = @"https://appsforoffice.microsoft.com/lib/1/hosted/office.js" + let _type = "text/javascript" + let s = Browser.Dom.document.createElement("script") + s?``type`` <- _type + s?src <- cdn + Browser.Dom.document.head.appendChild s |> ignore + s.onload <- fun _ -> callback() + () + + /// Make a function that loops short sleep sequences until a mutable variable is set to true + /// do mutabel dotnet ref for variable + let myAwaitLoadedThenInit(loaded: ref<bool>) = + let rec loop() = + async { + if loaded.Value then + do! initializeAddIn() + else + do! Async.Sleep 100 + do! loop() + } + loop() + + let officeload() = + let loaded = ref false + async { + addOfficeJsScript(fun _ -> loaded.Value <- true) + do! myAwaitLoadedThenInit loaded + } //open Fable.Core.JS @@ -29,24 +64,21 @@ module Interface = | Initialize host -> let cmd = Cmd.batch [ - Cmd.ofMsg (GetAppVersion |> Request |> Api) - Cmd.ofMsg (FetchAllOntologies |> Request |> Api) + Cmd.ofMsg (Ontologies.GetOntologies |> OntologyMsg) match host with | Swatehost.Excel -> - Cmd.OfPromise.either - OfficeInterop.Core.tryFindActiveAnnotationTable + Cmd.OfAsync.either + ExcelHelper.officeload () - (OfficeInterop.AnnotationTableExists >> OfficeInteropMsg) + (fun _ -> TryFindAnnotationTable |> OfficeInteropMsg) (curry GenericError Cmd.none >> DevMsg) | Swatehost.Browser -> - Cmd.batch [ - Cmd.ofEffect (fun dispatch -> Spreadsheet.KeyboardShortcuts.addOnKeydownEvent dispatch) - ] + Cmd.none | Swatehost.ARCitect -> - Cmd.batch [ - Cmd.ofEffect (fun dispatch -> Spreadsheet.KeyboardShortcuts.addOnKeydownEvent dispatch) - Cmd.ofEffect (fun _ -> ARCitect.ARCitect.send ARCitect.Init) - ] + Cmd.ofEffect (fun _ -> + LocalHistory.Model.ResetHistoryWebStorage() + ARCitect.ARCitect.send ARCitect.Init + ) ] model, cmd | CreateAnnotationTable usePrevOutput -> @@ -58,6 +90,16 @@ module Interface = let cmd = Spreadsheet.CreateAnnotationTable usePrevOutput |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" + | AddTable table -> + match host with + | Some Swatehost.Excel -> + //let cmd = OfficeInterop.AddTable table |> OfficeInteropMsg |> Cmd.ofMsg + failwith "AddTable not implemented for Excel" + model, Cmd.none + | Some Swatehost.Browser | Some Swatehost.ARCitect -> + let cmd = Spreadsheet.AddTable table |> SpreadsheetMsg |> Cmd.ofMsg + model, cmd + | _ -> failwith "not implemented" | AddAnnotationBlock minBuildingBlockInfo -> match host with //| Swatehost.Excel _ -> @@ -82,7 +124,7 @@ module Interface = let cmd = Spreadsheet.JoinTable (table, index, options) |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" - | ImportFile tables -> + | UpdateArcFile tables -> match host with | Some Swatehost.Excel -> //let cmd = OfficeInterop.ImportFile tables |> OfficeInteropMsg |> Cmd.ofMsg @@ -92,6 +134,15 @@ module Interface = let cmd = Spreadsheet.UpdateArcFile tables |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" + | ImportXlsx bytes -> + match host with + | Some Swatehost.Excel -> + Browser.Dom.window.alert "ImportXlsx Not implemented" + model, Cmd.none + | Some Swatehost.Browser | Some Swatehost.ARCitect -> + let cmd = Spreadsheet.ImportXlsx bytes |> SpreadsheetMsg |> Cmd.ofMsg + model, cmd + | _ -> failwith "not implemented" | InsertOntologyAnnotation termMinimal -> match host with //| Swatehost.Excel _ -> @@ -103,13 +154,24 @@ module Interface = | _ -> failwith "not implemented" | InsertFileNames fileNames -> match host with - | Some Swatehost.Excel | Some Swatehost.ARCitect -> + | Some Swatehost.Excel -> let cmd = OfficeInterop.InsertFileNames fileNames |> OfficeInteropMsg |> Cmd.ofMsg model, cmd - //| Swatehost.Browser -> - // let arr = fileNames |> List.toArray |> Array.map (fun x -> TermTypes.TermMinimal.create x "") - // let cmd = Spreadsheet.InsertOntologyTerms arr |> SpreadsheetMsg |> Cmd.ofMsg - // model, cmd + | Some Swatehost.Browser | Some Swatehost.ARCitect -> + if model.SpreadsheetModel.SelectedCells.IsEmpty then + model, Cmd.ofMsg (DevMsg.GenericError (Cmd.none, exn("No cell(s) selected.")) |> DevMsg) + else + let columnIndex, rowIndex = model.SpreadsheetModel.SelectedCells.MinimumElement + let mutable rowIndex = rowIndex + let cells = [| + for name in fileNames do + let c0 = model.SpreadsheetModel.ActiveTable.TryGetCellAt(columnIndex,rowIndex).Value + let cell = c0.UpdateMainField name + (columnIndex, rowIndex), cell + rowIndex <- rowIndex + 1 + |] + let cmd = Spreadsheet.UpdateCells cells |> SpreadsheetMsg |> Cmd.ofMsg + model, cmd | _ -> failwith "not implemented" | RemoveBuildingBlock -> match host with @@ -128,31 +190,13 @@ module Interface = Spreadsheet.DeleteColumn (distinct.[0]) |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" - | ExportJsonTable -> - match host with - | Some Swatehost.Excel -> - let cmd = JsonExporterMsg JsonExporter.ParseTableOfficeInteropRequest |> Cmd.ofMsg - model, cmd - | Some Swatehost.Browser -> - let cmd = SpreadsheetMsg Spreadsheet.ExportJsonTable |> Cmd.ofMsg - model, cmd - | _ -> failwith "not implemented" - | ExportJsonTables -> + | ExportJson (arcfile, jef) -> match host with | Some Swatehost.Excel -> - let cmd = JsonExporterMsg JsonExporter.ParseTablesOfficeInteropRequest |> Cmd.ofMsg - model, cmd + failwith "ExportJson not implemented for Excel" + model, Cmd.none | Some Swatehost.Browser -> - let cmd = SpreadsheetMsg Spreadsheet.ExportJsonTables |> Cmd.ofMsg - model, cmd - | _ -> failwith "not implemented" - | ParseTablesToDag -> - match host with - | Some Swatehost.Excel -> - let cmd = DagMsg Dag.ParseTablesOfficeInteropRequest |> Cmd.ofMsg - model, cmd - | Some Swatehost.Browser | Some Swatehost.ARCitect -> - let cmd = SpreadsheetMsg Spreadsheet.ParseTablesToDag |> Cmd.ofMsg + let cmd = SpreadsheetMsg (Spreadsheet.ExportJson (arcfile, jef)) |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" | EditBuildingBlock -> diff --git a/src/Client/Update/OfficeInteropUpdate.fs b/src/Client/Update/OfficeInteropUpdate.fs index f9c3e4d0..f6d0f093 100644 --- a/src/Client/Update/OfficeInteropUpdate.fs +++ b/src/Client/Update/OfficeInteropUpdate.fs @@ -9,9 +9,9 @@ open Shared open OfficeInteropTypes module OfficeInterop = - let update (currentModel:Messages.Model) (excelInteropMsg: OfficeInterop.Msg) : Messages.Model * Cmd<Messages.Msg> = + let update (model:Messages.Model) (msg: OfficeInterop.Msg) : Messages.Model * Cmd<Messages.Msg> = - match excelInteropMsg with + match msg with | AutoFitTable hidecols -> let p = fun () -> ExcelJS.Fable.GlobalBindings.Excel.run (OfficeInterop.Core.autoFitTable hidecols) @@ -21,18 +21,26 @@ module OfficeInterop = () (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd + | TryFindAnnotationTable -> + let cmd = + Cmd.OfPromise.either + OfficeInterop.Core.tryFindActiveAnnotationTable + () + (OfficeInterop.AnnotationTableExists >> OfficeInteropMsg) + (curry GenericError Cmd.none >> DevMsg) + model, cmd | AnnotationTableExists annoTableOpt -> let exists = match annoTableOpt with | Success name -> true | _ -> false let nextState = { - currentModel.ExcelState with + model.ExcelState with HasAnnotationTable = exists } - currentModel.updateByExcelState nextState,Cmd.none + model.updateByExcelState nextState,Cmd.none | InsertOntologyTerm (term) -> let cmd = @@ -41,7 +49,7 @@ module OfficeInterop = term (curry GenericLog Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | AddAnnotationBlock (minBuildingBlockInfo) -> let cmd = @@ -50,7 +58,7 @@ module OfficeInterop = (minBuildingBlockInfo) (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | AddAnnotationBlocks minBuildingBlockInfos -> let cmd = @@ -59,7 +67,7 @@ module OfficeInterop = minBuildingBlockInfos (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | ImportFile buildingBlockTables -> let nextCmd = @@ -68,7 +76,7 @@ module OfficeInterop = buildingBlockTables (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, nextCmd + model, nextCmd | RemoveBuildingBlock -> let cmd = @@ -77,7 +85,7 @@ module OfficeInterop = () (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | UpdateUnitForCells (unitTerm) -> let cmd = @@ -86,7 +94,7 @@ module OfficeInterop = unitTerm (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | CreateAnnotationTable(tryUsePrevOutput) -> let cmd = @@ -95,14 +103,14 @@ module OfficeInterop = (false,tryUsePrevOutput) (curry GenericInteropLogs (AnnotationtableCreated |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel,cmd + model,cmd | AnnotationtableCreated -> let nextState = { - currentModel.ExcelState with + model.ExcelState with HasAnnotationTable = true } - currentModel.updateByExcelState nextState, Cmd.none + model.updateByExcelState nextState, Cmd.none | GetParentTerm -> @@ -110,33 +118,34 @@ module OfficeInterop = Cmd.OfPromise.either OfficeInterop.Core.getParentTerm () - (fun tmin -> tmin |> Option.map (fun t -> ARCtrl.ISA.OntologyAnnotation.fromTerm t.toTerm) |> TermSearch.UpdateParentTerm |> TermSearchMsg) + (fun tmin -> tmin |> Option.map (fun t -> ARCtrl.OntologyAnnotation.fromTerm t.toTerm) |> TermSearch.UpdateParentTerm |> TermSearchMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd // | FillHiddenColsRequest -> - let cmd = - Cmd.OfPromise.either - OfficeInterop.Core.getAllAnnotationBlockDetails - () - (fun (searchTerms,deprecationLogs) -> - // Push possible deprecation messages by piping through "GenericInteropLogs" - GenericInteropLogs ( - // This will be executed after "deprecationLogs" are handled by "GenericInteropLogs" - SearchForInsertTermsRequest searchTerms |> Request |> Api |> Cmd.ofMsg, - // This will be pushed to Activity logs, or as wanring modal to user in case of LogIdentifier.Warning - deprecationLogs - ) - |> DevMsg - ) - (curry GenericError (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) - let stateCmd = UpdateFillHiddenColsState FillHiddenColsState.ExcelCheckHiddenCols |> OfficeInteropMsg |> Cmd.ofMsg - let cmds = Cmd.batch [cmd; stateCmd] - currentModel, cmds + failwith "FillHiddenColsRequest Not implemented yet" + //let cmd = + // Cmd.OfPromise.either + // OfficeInterop.Core.getAllAnnotationBlockDetails + // () + // (fun (searchTerms,deprecationLogs) -> + // // Push possible deprecation messages by piping through "GenericInteropLogs" + // GenericInteropLogs ( + // // This will be executed after "deprecationLogs" are handled by "GenericInteropLogs" + // SearchForInsertTermsRequest searchTerms |> Request |> Api |> Cmd.ofMsg, + // // This will be pushed to Activity logs, or as wanring modal to user in case of LogIdentifier.Warning + // deprecationLogs + // ) + // |> DevMsg + // ) + // (curry GenericError (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) + //let stateCmd = UpdateFillHiddenColsState FillHiddenColsState.ExcelCheckHiddenCols |> OfficeInteropMsg |> Cmd.ofMsg + //let cmds = Cmd.batch [cmd; stateCmd] + model, Cmd.none | FillHiddenColumns (termsWithSearchResult) -> let nextState = { - currentModel.ExcelState with + model.ExcelState with FillHiddenColsStateStore = FillHiddenColsState.ExcelWriteFoundTerms } let cmd = @@ -145,15 +154,15 @@ module OfficeInterop = (termsWithSearchResult) (curry GenericInteropLogs (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) (curry GenericError (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByExcelState nextState, cmd + model.updateByExcelState nextState, cmd | UpdateFillHiddenColsState newState -> let nextState = { - currentModel.ExcelState with + model.ExcelState with FillHiddenColsStateStore = newState } - currentModel.updateByExcelState nextState, Cmd.none + model.updateByExcelState nextState, Cmd.none // | InsertFileNames (fileNameList) -> let cmd = @@ -162,7 +171,7 @@ module OfficeInterop = (fileNameList) (curry GenericLog Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd // | GetSelectedBuildingBlockTerms -> @@ -178,7 +187,7 @@ module OfficeInterop = ] ) (curry GenericError (UpdateCurrentRequestState RequestBuildingBlockInfoStates.Inactive |> BuildingBlockDetails |> Cmd.ofMsg) >> DevMsg) - currentModel, cmd + model, cmd // DEV | TryExcel -> @@ -188,7 +197,7 @@ module OfficeInterop = () ((fun x -> curry GenericLog Cmd.none ("Debug",x)) >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | TryExcel2 -> let cmd = Cmd.OfPromise.either @@ -196,7 +205,7 @@ module OfficeInterop = () ((fun x -> curry GenericLog Cmd.none ("Debug",x)) >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd //| _ -> // printfn "Hit currently non existing message" // currentState, Cmd.none \ No newline at end of file diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index e81fb52c..614bbd8c 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -7,15 +7,14 @@ open LocalHistory open Model open Shared open Spreadsheet.Table -open Spreadsheet.Sidebar +open Spreadsheet.BuildingBlocks open Spreadsheet.Clipboard open Fable.Remoting.Client -open Fable.Remoting.Client.InternalUtilities open FsSpreadsheet -open FsSpreadsheet.Exceljs -open ARCtrl.ISA -open ARCtrl.ISA.Spreadsheet -open Spreadsheet.Sidebar.Controller +open FsSpreadsheet.Js +open ARCtrl +open ARCtrl.Spreadsheet +open ARCtrl.Json module Spreadsheet = @@ -27,6 +26,10 @@ module Spreadsheet = let download(filename, bytes:byte []) = bytes.SaveFileAs(filename) + let downloadFromString(filename, content:string) = + let bytes = System.Text.Encoding.UTF8.GetBytes(content) + bytes.SaveFileAs(filename) + /// <summary> /// This function will store the information correctly. /// Can return save information to local storage (persistent between browser sessions) and session storage. @@ -34,18 +37,21 @@ module Spreadsheet = /// </summary> let updateHistoryStorageMsg (msg: Spreadsheet.Msg) (state: Spreadsheet.Model, model: Messages.Model, cmd) = match msg with - | UpdateActiveView _ | UpdateHistoryPosition _ | Reset | UpdateSelectedCells _ | CopySelectedCell | CopyCell _ -> + | UpdateActiveView _ | UpdateHistoryPosition _ | Reset | UpdateSelectedCells _ + | UpdateActiveCell _ | CopySelectedCell | CopyCell _ | MoveSelectedCell _ | SetActiveCellFromSelected -> state.SaveToLocalStorage() // This will cache the most up to date table state to local storage. state, model, cmd | _ -> state.SaveToLocalStorage() // This will cache the most up to date table state to local storage. let nextHistory = model.History.SaveSessionSnapshot state // this will cache the table state for certain operations in session storage. if model.PersistentStorageState.Host = Some Swatehost.ARCitect then - match model.SpreadsheetModel.ArcFile with + match state.ArcFile with // model is not yet updated at this position. | Some (Assay assay) -> ARCitect.ARCitect.send(ARCitect.AssayToARCitect assay) | Some (Study (study,_)) -> ARCitect.ARCitect.send(ARCitect.StudyToARCitect study) + | Some (Investigation inv) -> + ARCitect.ARCitect.send(ARCitect.InvestigationToARCitect inv) | _ -> () state, {model with History = nextHistory}, cmd @@ -66,6 +72,11 @@ module Spreadsheet = let innerUpdate (state: Spreadsheet.Model) (model: Messages.Model) (msg: Spreadsheet.Msg) = match msg with + | UpdateState nextState -> + nextState, model, Cmd.none + | AddTable table -> + let nextState = Controller.addTable table state + nextState, model, Cmd.none | CreateAnnotationTable usePrevOutput -> let nextState = Controller.createTable usePrevOutput state nextState, model, Cmd.none @@ -82,7 +93,7 @@ module Spreadsheet = let nextState = { state with ArcFile = Some arcFile } nextState, model, Cmd.none | InitFromArcFile arcFile -> - let nextState = { Spreadsheet.Model.init() with ArcFile = Some arcFile } + let nextState = Spreadsheet.Model.init(arcFile) nextState, model, Cmd.none | InsertOntologyAnnotation oa -> let nextState = Controller.insertTerm_IntoSelected oa state @@ -97,13 +108,22 @@ module Spreadsheet = state.ActiveTable.UpdateCellAt(fst index,snd index, cell) {state with ArcFile = state.ArcFile} nextState, model, Cmd.none + | UpdateCells arr -> + let nextState = + state.ActiveTable.SetCellsAt arr + {state with ArcFile = state.ArcFile} + nextState, model, Cmd.none | UpdateHeader (index, header) -> let nextState = state.ActiveTable.UpdateHeader(index, header) {state with ArcFile = state.ArcFile} nextState, model, Cmd.none | UpdateActiveView nextView -> - let nextState = { state with ActiveView = nextView } + let nextState = { + state with + ActiveView = nextView + SelectedCells = Set.empty + } nextState, model, Cmd.none | RemoveTable removeIndex -> let nextState = Controller.removeTable removeIndex state @@ -131,8 +151,8 @@ module Spreadsheet = let nextState = Controller.addRows n state nextState, model, Cmd.none | Reset -> - let nextHistory, nextState = Controller.resetTableState() - let nextModel = {model with History = nextHistory} + let nextState = Controller.resetTableState() + let nextModel = {model with History = LocalHistory.Model.init()} nextState, nextModel, Cmd.none | DeleteRow index -> let nextState = Controller.deleteRow index state @@ -143,32 +163,121 @@ module Spreadsheet = | DeleteColumn index -> let nextState = Controller.deleteColumn index state nextState, model, Cmd.none + | SetColumn (index, column) -> + let nextState = Controller.setColumn index column state + nextState, model, Cmd.none + | MoveColumn (current, next) -> + let nextState = Controller.moveColumn current next state + nextState, model, Cmd.none | UpdateSelectedCells nextSelectedCells -> let nextState = {state with SelectedCells = nextSelectedCells} nextState, model, Cmd.none - | CopyCell index -> - let nextState = Controller.copyCell index state + | MoveSelectedCell keypressed -> + let cmd = + match state.SelectedCells.IsEmpty with + | true -> Cmd.none + | false -> + let moveBy = + match keypressed with + | Key.Down -> (0,1) + | Key.Up -> (0,-1) + | Key.Left -> (-1,0) + | Key.Right -> (1,0) + let nextIndex = Controller.selectRelativeCell state.SelectedCells.MinimumElement moveBy state.ActiveTable + let s = Set([nextIndex]) + UpdateSelectedCells s |> SpreadsheetMsg |> Cmd.ofMsg + state, model, cmd + | SetActiveCellFromSelected -> + let cmd = + if state.SelectedCells.IsEmpty then + Cmd.none + else + let min = state.SelectedCells.MinimumElement + let cmd = (Fable.Core.U2.Case2 min, ColumnType.Main) |> Some |> UpdateActiveCell |> SpreadsheetMsg + Cmd.ofMsg cmd + state, model, cmd + | UpdateActiveCell next -> + let nextState = { state with ActiveCell = next } nextState, model, Cmd.none + | CopyCell index -> + let cmd = + Cmd.OfPromise.attempt + (Controller.copyCellByIndex index) + state + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | CopyCells indices -> + let cmd = + Cmd.OfPromise.attempt + (Controller.copyCellsByIndex indices) + state + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd | CopySelectedCell -> - let nextState = - if state.SelectedCells.IsEmpty then state else - Controller.copySelectedCell state - nextState, model, Cmd.none + let cmd = + Cmd.OfPromise.attempt + (Controller.copySelectedCell) + state + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | CopySelectedCells -> + let cmd = + Cmd.OfPromise.attempt + (Controller.copySelectedCells) + state + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd | CutCell index -> - let nextState = Controller.cutCell index state + let nextState = Controller.cutCellByIndex index state nextState, model, Cmd.none | CutSelectedCell -> let nextState = if state.SelectedCells.IsEmpty then state else Controller.cutSelectedCell state nextState, model, Cmd.none - | PasteCell index -> - let nextState = if state.Clipboard.Cell.IsNone then state else Controller.pasteCell index state + | CutSelectedCells -> + let nextState = + if state.SelectedCells.IsEmpty then state else + Controller.cutSelectedCells state nextState, model, Cmd.none + | PasteCell index -> + let cmd = + Cmd.OfPromise.either + (Clipboard.Controller.pasteCellByIndex index) + state + (UpdateState >> SpreadsheetMsg) + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | PasteCellsExtend index -> + let cmd = + Cmd.OfPromise.either + (Clipboard.Controller.pasteCellsByIndexExtend index) + state + (UpdateState >> SpreadsheetMsg) + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd | PasteSelectedCell -> - let nextState = - if state.SelectedCells.IsEmpty || state.Clipboard.Cell.IsNone then state else - Controller.pasteSelectedCell state + let cmd = + Cmd.OfPromise.either + (Clipboard.Controller.pasteCellIntoSelected) + state + (UpdateState >> SpreadsheetMsg) + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | PasteSelectedCells -> + let cmd = + Cmd.OfPromise.either + (Clipboard.Controller.pasteCellsIntoSelected) + state + (UpdateState >> SpreadsheetMsg) + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | Clear indices -> + let nextState = Controller.clearCells indices state + nextState, model, Cmd.none + | ClearSelected -> + let indices = state.SelectedCells |> Set.toArray + let nextState = Controller.clearCells indices state nextState, model, Cmd.none | FillColumnWithTerm index -> let nextState = Controller.fillColumnWithCell index state @@ -176,63 +285,41 @@ module Spreadsheet = //| EditColumn (columnIndex, newCellType, b_type) -> // let cmd = createPromiseCmd <| fun _ -> Controller.editColumn (columnIndex, newCellType, b_type) state // state, model, cmd - | SetArcFileFromBytes bytes -> + | ImportXlsx bytes -> let cmd = Cmd.OfPromise.either - Spreadsheet.IO.readFromBytes + Spreadsheet.IO.Xlsx.readFromBytes bytes (UpdateArcFile >> Messages.SpreadsheetMsg) (Messages.curry Messages.GenericError Cmd.none >> Messages.DevMsg) state, model, cmd - | ExportJsonTable -> - failwith "ExportsJsonTable is not implemented" - //let exportJsonState = {model.JsonExporterModel with Loading = true} - //let nextModel = model.updateByJsonExporterModel exportJsonState - //let func() = promise { - // return Controller.getTable state - //} - //let cmd = - // Cmd.OfPromise.either - // func - // () - // (JsonExporter.State.ParseTableServerRequest >> Messages.JsonExporterMsg) - // (Messages.curry Messages.GenericError (JsonExporter.State.UpdateLoading false |> Messages.JsonExporterMsg |> Cmd.ofMsg) >> Messages.DevMsg) - //state, nextModel, cmd - state, model, Cmd.none - | ExportJsonTables -> - failwith "ExportJsonTables is not implemented" - //let exportJsonState = {model.JsonExporterModel with Loading = true} - //let nextModel = model.updateByJsonExporterModel exportJsonState - //let func() = promise { - // return Controller.getTables state - //} - //let cmd = - // Cmd.OfPromise.either - // func - // () - // (JsonExporter.State.ParseTablesServerRequest >> Messages.JsonExporterMsg) - // (Messages.curry Messages.GenericError (JsonExporter.State.UpdateLoading false |> Messages.JsonExporterMsg |> Cmd.ofMsg) >> Messages.DevMsg) - //state, nextModel, cmd - state, model, Cmd.none - | ParseTablesToDag -> - failwith "ParseTablesToDag is not implemented" - //let dagState = {model.DagModel with Loading = true} - //let nextModel = model.updateByDagModel dagState - //let func() = promise { - // return Controller.getTables state - //} - //let cmd = - // Cmd.OfPromise.either - // func - // () - // (Dag.ParseTablesDagServerRequest >> Messages.DagMsg) - // (Messages.curry Messages.GenericError (Dag.UpdateLoading false |> Messages.DagMsg |> Cmd.ofMsg) >> Messages.DevMsg) - //state, nextModel, cmd + | ExportJson (arcfile,jef) -> + let name, jsonString = + let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") + let nameFromId (id: string) = (n + "_" + id + ".json") + match arcfile, jef with + | Investigation ai, JsonExportFormat.ARCtrl -> nameFromId ai.Identifier, ArcInvestigation.toJsonString 0 ai + | Investigation ai, JsonExportFormat.ARCtrlCompressed -> nameFromId ai.Identifier, ArcInvestigation.toCompressedJsonString 0 ai + | Investigation ai, JsonExportFormat.ISA -> nameFromId ai.Identifier, ArcInvestigation.toISAJsonString 0 ai + | Investigation ai, JsonExportFormat.ROCrate -> nameFromId ai.Identifier, ArcInvestigation.toROCrateJsonString 0 ai + + | Study (as',_), JsonExportFormat.ARCtrl -> nameFromId as'.Identifier, ArcStudy.toJsonString 0 (as') + | Study (as',_), JsonExportFormat.ARCtrlCompressed -> nameFromId as'.Identifier, ArcStudy.toCompressedJsonString 0 (as') + | Study (as',aaList), JsonExportFormat.ISA -> nameFromId as'.Identifier, ArcStudy.toISAJsonString (aaList,0) (as') + | Study (as',aaList), JsonExportFormat.ROCrate -> nameFromId as'.Identifier, ArcStudy.toROCrateJsonString (aaList,0) (as') + + | Assay aa, JsonExportFormat.ARCtrl -> nameFromId aa.Identifier, ArcAssay.toJsonString 0 aa + | Assay aa, JsonExportFormat.ARCtrlCompressed -> nameFromId aa.Identifier, ArcAssay.toCompressedJsonString 0 aa + | Assay aa, JsonExportFormat.ISA -> nameFromId aa.Identifier, ArcAssay.toISAJsonString 0 aa + | Assay aa, JsonExportFormat.ROCrate -> nameFromId aa.Identifier, ArcAssay.toROCrateJsonString () aa + + | Template t, JsonExportFormat.ARCtrl -> nameFromId t.FileName, Template.toJsonString 0 t + | Template t, JsonExportFormat.ARCtrlCompressed -> nameFromId t.FileName, Template.toCompressedJsonString 0 t + | Template _, anyElse -> failwithf "Error. It is not intended to parse Template to %s format." (string anyElse) + Helper.downloadFromString (name , jsonString) + state, model, Cmd.none | ExportXlsx arcfile-> - // we highjack this loading function - let exportJsonState = {model.JsonExporterModel with Loading = true} - let nextModel = model.updateByJsonExporterModel exportJsonState let name, fswb = let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") match arcfile with @@ -243,22 +330,17 @@ module Spreadsheet = | Assay aa -> n + "_" + ArcAssay.FileName, ArcAssay.toFsWorkbook aa | Template t -> - n + "_" + t.FileName, ARCtrl.Template.Spreadsheet.Template.toFsWorkbook t + n + "_" + t.FileName, Spreadsheet.Template.toFsWorkbook t let cmd = Cmd.OfPromise.either - FsSpreadsheet.Exceljs.Xlsx.toBytes + Xlsx.toXlsxBytes fswb (fun bytes -> ExportXlsxDownload (name,bytes) |> Messages.SpreadsheetMsg) - (Messages.curry Messages.GenericError (JsonExporter.UpdateLoading false |> Messages.JsonExporterMsg |> Cmd.ofMsg) >> Messages.DevMsg) - state, nextModel, cmd + (Messages.curry Messages.GenericError Cmd.none >> Messages.DevMsg) + state, model, cmd | ExportXlsxDownload (name,xlsxBytes) -> let _ = Helper.download (name ,xlsxBytes) - let nextJsonExporter = { - model.JsonExporterModel with - Loading = false - } - let nextModel = model.updateByJsonExporterModel nextJsonExporter - state, nextModel, Cmd.none + state, model, Cmd.none | UpdateTermColumns -> //let getUpdateTermColumns() = promise { // return Controller.getUpdateTermColumns state diff --git a/src/Client/Views/MainWindowView.fs b/src/Client/Views/MainWindowView.fs index 396c6925..795e8fbb 100644 --- a/src/Client/Views/MainWindowView.fs +++ b/src/Client/Views/MainWindowView.fs @@ -4,8 +4,29 @@ open Feliz open Feliz.Bulma open Messages open Shared +open MainComponents +open Shared +open Fable.Core.JsInterop + +let private WidgetOrderContainer bringWidgetToFront (widget) = + Html.div [ + prop.onClick bringWidgetToFront + prop.children [ + widget + ] + ] + +let private ModalDisplay (widgets: Widget list, displayWidget: Widget -> ReactElement) = + + match widgets.Length with + | 0 -> + Html.none + | _ -> + Html.div [ + for widget in widgets do displayWidget widget + ] -let private spreadsheetSelectionFooter (model: Messages.Model) dispatch = +let private SpreadsheetSelectionFooter (model: Messages.Model) dispatch = Html.div [ prop.style [ style.position.sticky; @@ -15,23 +36,19 @@ let private spreadsheetSelectionFooter (model: Messages.Model) dispatch = Html.div [ prop.children [ Bulma.tabs [ - prop.style [style.overflowY.visible] Bulma.tabs.isBoxed prop.children [ Html.ul [ - yield Bulma.tab [ + Bulma.tab [ prop.style [style.width (length.px 20)] ] - yield - MainComponents.FooterTabs.MainMetadata {| model=model; dispatch = dispatch |} + MainComponents.FooterTabs.MainMetadata (model, dispatch) for index in 0 .. (model.SpreadsheetModel.Tables.TableCount-1) do - yield - MainComponents.FooterTabs.Main {| index = index; tables = model.SpreadsheetModel.Tables; model = model; dispatch = dispatch |} - match model.SpreadsheetModel.ArcFile with - | Some (ArcFiles.Template _) | Some (ArcFiles.Investigation _) -> - yield Html.none - | _ -> - yield MainComponents.FooterTabs.MainPlus {| dispatch = dispatch |} + MainComponents.FooterTabs.Main (index, model.SpreadsheetModel.Tables, model, dispatch) + if model.SpreadsheetModel.CanHaveTables() then + MainComponents.FooterTabs.MainPlus (model, dispatch) + if model.SpreadsheetModel.TableViewIsActive() then + MainComponents.FooterTabs.ToggleSidebar(model, dispatch) ] ] ] @@ -43,7 +60,22 @@ let private spreadsheetSelectionFooter (model: Messages.Model) dispatch = open Shared [<ReactComponent>] -let Main (model: Messages.Model) dispatch = +let Main (model: Messages.Model, dispatch) = + let widgets, setWidgets = React.useState([]) + let rmvWidget (widget: Widget) = widgets |> List.except [widget] |> setWidgets + let bringWidgetToFront (widget: Widget) = + let newList = widgets |> List.except [widget] |> fun x -> widget::x |> List.rev + setWidgets newList + let displayWidget (widget: Widget) = + let rmv (widget: Widget) = fun _ -> rmvWidget widget + let bringWidgetToFront = fun _ -> bringWidgetToFront widget + match widget with + | Widget._BuildingBlock -> Widget.BuildingBlock (model, dispatch, rmv widget) + | Widget._Template -> Widget.Templates (model, dispatch, rmv widget) + | Widget._FilePicker -> Widget.FilePicker (model, dispatch, rmv widget) + |> WidgetOrderContainer bringWidgetToFront + let addWidget (widget: Widget) = + widget::widgets |> List.rev |> setWidgets let state = model.SpreadsheetModel Html.div [ prop.id "MainWindow" @@ -54,7 +86,8 @@ let Main (model: Messages.Model) dispatch = style.height (length.percent 100) ] prop.children [ - MainComponents.Navbar.Main model dispatch + MainComponents.Navbar.Main (model, dispatch, widgets, setWidgets) + ModalDisplay (widgets, displayWidget) Html.div [ prop.id "TableContainer" prop.style [ @@ -73,12 +106,13 @@ let Main (model: Messages.Model) dispatch = | Some (ArcFiles.Study _) | Some (ArcFiles.Investigation _) | Some (ArcFiles.Template _) -> - XlsxFileView.Main {|model = model; dispatch = dispatch|} + Html.none + XlsxFileView.Main (model, dispatch, (fun () -> addWidget Widget._BuildingBlock), (fun () -> addWidget Widget._Template)) if state.Tables.TableCount > 0 && state.ActiveTable.ColumnCount > 0 && state.ActiveView <> Spreadsheet.ActiveView.Metadata then MainComponents.AddRows.Main dispatch ] ] if state.ArcFile.IsSome then - spreadsheetSelectionFooter model dispatch + SpreadsheetSelectionFooter model dispatch ] ] \ No newline at end of file diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index 37284b30..a8822137 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -30,6 +30,7 @@ let private createNavigationTab (pageLink: Routing.Route) (model:Model) (dispatc Bulma.tab [ if isActive then Bulma.tab.isActive Html.a [ + prop.className "navigation" // this class does not do anything, but disables <a> styling. prop.onClick (fun e -> e.preventDefault(); UpdatePageState (Some pageLink) |> dispatch) match sidebarsize with | Mini | MobileMini -> @@ -55,30 +56,13 @@ let private tabs (model:Model) dispatch (sidebarsize: Model.WindowSize) = let isIEBrowser : bool = Browser.Dom.window.document?documentMode tabRow model [ if not model.PageState.IsExpert then - createNavigationTab Routing.Route.BuildingBlock model dispatch sidebarsize - createNavigationTab Routing.Route.TermSearch model dispatch sidebarsize - createNavigationTab Routing.Route.Protocol model dispatch sidebarsize - createNavigationTab Routing.Route.FilePicker model dispatch sidebarsize - //if not isIEBrowser then - // docsrc attribute not supported in iframe in IE - //createNavigationTab Routing.Route.Dag model dispatch sidebarsize - createNavigationTab Routing.Route.Info model dispatch sidebarsize + createNavigationTab Routing.Route.BuildingBlock model dispatch sidebarsize + createNavigationTab Routing.Route.TermSearch model dispatch sidebarsize + createNavigationTab Routing.Route.Protocol model dispatch sidebarsize + createNavigationTab Routing.Route.FilePicker model dispatch sidebarsize + createNavigationTab Routing.Route.JsonExport model dispatch sidebarsize else - createNavigationTab Routing.Route.JsonExport model dispatch sidebarsize - createNavigationTab Routing.Route.TemplateMetadata model dispatch sidebarsize - //createNavigationTab Routing.Route.Validation model dispatch sidebarsize - createNavigationTab Routing.Route.Info model dispatch sidebarsize - ] - - -let private footer (model:Model) = - div [Style [Color "grey"; Position PositionOptions.Sticky; Width "inherit"; Bottom "0"; TextAlign TextAlignOptions.Center ]] [ - div [] [ - str "Swate Release Version " - a [Href "https://github.com/nfdi4plants/Swate/releases"] [str model.PersistentStorageState.AppVersion] - str " Host " - Html.span [prop.style [style.color "#4fb3d9"]; prop.text (sprintf "%O" model.PersistentStorageState.Host)] - ] + createNavigationTab Routing.Route.JsonExport model dispatch sidebarsize ] module private ResizeObserver = @@ -131,11 +115,6 @@ let private viewContainer (model: Model) (dispatch: Msg -> unit) (state: Sidebar let ele = Browser.Dom.document.getElementById(Sidebar_Id) ResizeObserver.observer(state, setState).observe(ele) ) - OnClick (fun e -> - if model.AddBuildingBlockState.ShowUnit2TermSuggestions - then - TopLevelMsg.CloseSuggestions |> TopLevelMsg |> dispatch - ) Style [ Display DisplayOptions.Flex FlexGrow "1" @@ -146,9 +125,27 @@ let private viewContainer (model: Model) (dispatch: Msg -> unit) (state: Sidebar ] ] children +type SidebarView = + + [<ReactComponent>] + static member private footer (model:Model, dispatch) = + React.useEffectOnce(fun () -> + async { + let! versionResponse = Api.serviceApi.getAppVersion() + PersistentStorage.UpdateAppVersion versionResponse |> PersistentStorageMsg |> dispatch + } + |> Async.StartImmediate + ) + div [Style [Color "grey"; Position PositionOptions.Sticky; Width "inherit"; Bottom "0"; TextAlign TextAlignOptions.Center ]] [ + div [] [ + str "Swate Release Version " + a [Href "https://github.com/nfdi4plants/Swate/releases"; HTMLAttr.Target "_Blank"] [str model.PersistentStorageState.AppVersion] + str " Host " + Html.a [prop.style [style.cursor.defaultCursor] ;prop.text (sprintf "%O" model.PersistentStorageState.Host)] + ] + ] -module private Content = - let main (model:Model) (dispatch: Msg -> unit) = + static member private content (model:Model) (dispatch: Msg -> unit) = match model.PageState.CurrentPage with | Routing.Route.BuildingBlock | Routing.Route.Home _ -> BuildingBlock.Core.addBuildingBlockComponent model dispatch @@ -160,16 +157,13 @@ module private Content = FilePicker.filePickerComponent model dispatch | Routing.Route.Protocol -> - Protocol.Core.fileUploadViewComponent model dispatch + Protocol.Templates.Main (model, dispatch) | Routing.Route.JsonExport -> - JsonExporter.Core.jsonExporterMainElement model dispatch - - | Routing.Route.TemplateMetadata -> - TemplateMetadata.Core.newNameMainElement model dispatch + JsonExporter.Core.FileExporter.Main(model, dispatch) | Routing.Route.ProtocolSearch -> - Protocol.Search.ProtocolSearchView model dispatch + Protocol.Search.Main model dispatch | Routing.Route.ActivityLog -> ActivityLog.activityLogComponent model dispatch @@ -177,49 +171,43 @@ module private Content = | Routing.Route.Settings -> SettingsView.settingsViewComponent model dispatch - //| Routing.Route.SettingsXml -> - // SettingsXml.settingsXmlViewComponent model dispatch - - | Routing.Route.Dag -> - Dag.Core.mainElement model dispatch - | Routing.Route.Info -> InfoView.infoComponent model dispatch | Routing.Route.NotFound -> NotFoundView.notFoundComponent model dispatch -/// The base react component for the sidebar view in the app. contains the navbar and takes body and footer components to create the full view. -[<ReactComponent>] -let SidebarView (model: Model) (dispatch: Msg -> unit) = - let state, setState = React.useState(SidebarStyle.init) - viewContainer model dispatch state setState [ - SidebarComponents.Navbar.NavbarComponent model dispatch state.Size - - Bulma.container [ - Bulma.container.isFluid - prop.className "pl-4 pr-4" - prop.children [ - tabs model dispatch state.Size - - //str <| state.Size.ToString() - - //Button.button [ - // Button.OnClick (fun _ -> - // //Spreadsheet.Controller.deleteRow 2 model.SpreadsheetModel - // //() - // //Spreadsheet.DeleteColumn 1 |> SpreadsheetMsg |> dispatch - // () - // ) - //] [ str "Test button" ] - - match model.PersistentStorageState.Host, not model.ExcelState.HasAnnotationTable with - | Some Swatehost.Excel, true -> - SidebarComponents.AnnotationTableMissingWarning.annotationTableMissingWarningComponent model dispatch - | _ -> () - - Content.main model dispatch + /// The base react component for the sidebar view in the app. contains the navbar and takes body and footer components to create the full view. + [<ReactComponent>] + static member Main (model: Model, dispatch: Msg -> unit) = + let state, setState = React.useState(SidebarStyle.init) + viewContainer model dispatch state setState [ + SidebarComponents.Navbar.NavbarComponent model dispatch state.Size + + Bulma.container [ + Bulma.container.isFluid + prop.className "pl-4 pr-4" + prop.children [ + tabs model dispatch state.Size + + //str <| state.Size.ToString() + + //Button.button [ + // Button.OnClick (fun _ -> + // //Spreadsheet.Controller.deleteRow 2 model.SpreadsheetModel + // //() + // //Spreadsheet.DeleteColumn 1 |> SpreadsheetMsg |> dispatch + // () + // ) + //] [ str "Test button" ] + + match model.PersistentStorageState.Host, not model.ExcelState.HasAnnotationTable with + | Some Swatehost.Excel, true -> + SidebarComponents.AnnotationTableMissingWarning.annotationTableMissingWarningComponent model dispatch + | _ -> () + + SidebarView.content model dispatch + ] ] - ] - footer model - ] \ No newline at end of file + SidebarView.footer (model, dispatch) + ] \ No newline at end of file diff --git a/src/Client/Views/SplitWindowView.fs b/src/Client/Views/SplitWindowView.fs index 379b3d9c..7378388e 100644 --- a/src/Client/Views/SplitWindowView.fs +++ b/src/Client/Views/SplitWindowView.fs @@ -1,6 +1,7 @@ module SplitWindowView open Feliz +open Feliz.Bulma open Elmish open Browser.Types open LocalStorage.SplitWindow @@ -20,10 +21,12 @@ let private calculateNewSideBarSize (model:SplitWindow) (pos:float) = let private onResize_event (model:SplitWindow) (setModel: SplitWindow -> unit) = (fun (e: Event) -> /// must get width like this, cannot propagate model correctly. - let sidebarWindow = Browser.Dom.document.getElementById(sidebarId).clientWidth - let windowWidth = Browser.Dom.window.innerWidth - let new_sidebarWidth = calculateNewSideBarSize model (windowWidth - sidebarWindow) - { model with RightWindowWidth = new_sidebarWidth } |> setModel + let ele = Browser.Dom.document.getElementById(sidebarId) + if isNull ele |> not then + let sidebarWindow = ele.clientWidth + let windowWidth = Browser.Dom.window.innerWidth + let new_sidebarWidth = calculateNewSideBarSize model (windowWidth - sidebarWindow) + { model with RightWindowWidth = new_sidebarWidth } |> setModel ) /// <summary> This event changes the size of main window and sidebar </summary> @@ -74,12 +77,31 @@ let exampleTerm = false "MS" +let private sidebarCombinedElement(sidebarId: string, model: SplitWindow, setModel, dispatch, right) = + Html.div [ + prop.id sidebarId + prop.style [ + style.float'.right; + style.minWidth(minWidth); + style.flexBasis(length.px model.RightWindowWidth); style.flexShrink 0; style.flexGrow 0 + style.height(length.vh 100) + style.width(length.perc 100) + style.overflow.hidden + style.display.flex + ] + prop.children [ + dragbar model setModel dispatch + yield! right + ] + ] + // https://jsfiddle.net/gaby/Bek9L/ // https://stackoverflow.com/questions/6219031/how-can-i-resize-a-div-by-dragging-just-one-side-of-it /// Splits screen into two parts. Left and right, with a dragbar in between to change size of right side. [<ReactComponent>] let Main (left:seq<Fable.React.ReactElement>) (right:seq<Fable.React.ReactElement>) (mainModel:Messages.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 [ @@ -87,32 +109,8 @@ let Main (left:seq<Fable.React.ReactElement>) (right:seq<Fable.React.ReactElemen style.display.flex ] prop.children [ - Html.div [ - prop.style [ - style.minWidth(minWidth) - style.flexGrow 1 - style.flexShrink 1 - style.height(length.vh 100) - style.width(length.perc 100) - ] - prop.children left - ] - if not (mainModel.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Metadata) then - Html.div [ - prop.id sidebarId - prop.style [ - style.float'.right; - style.minWidth(minWidth); - style.flexBasis(length.px model.RightWindowWidth); style.flexShrink 0; style.flexGrow 0 - style.height(length.vh 100) - style.width(length.perc 100) - style.overflow.hidden - style.display.flex - ] - prop.children [ - dragbar model setModel dispatch - yield! right - ] - ] + MainComponents.MainViewContainer.Main(minWidth, left) + if isNotMetadataSheet && 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 c46e32ef..d530d332 100644 --- a/src/Client/Views/XlsxFileView.fs +++ b/src/Client/Views/XlsxFileView.fs @@ -1,4 +1,4 @@ -module XlsxFileView +module XlsxFileView open Feliz open Feliz.Bulma @@ -7,11 +7,14 @@ open Spreadsheet open Shared [<ReactComponentAttribute>] -let Main(x: {| model: Messages.Model; dispatch: Messages.Msg -> unit |}) = - let model, dispatch = x.model, x.dispatch +let Main(model: Messages.Model, dispatch: Messages.Msg -> unit, openBuildingBlockWidget, openTemplateWidget) = match model.SpreadsheetModel.ActiveView with | ActiveView.Table _ -> - MainComponents.SpreadsheetView.Main model dispatch + match model.SpreadsheetModel.ActiveTable.ColumnCount with + | 0 -> + MainComponents.EmptyTableElement.Main(openBuildingBlockWidget, openTemplateWidget) + | _ -> + MainComponents.SpreadsheetView.Main model dispatch | ActiveView.Metadata -> Bulma.section [ Bulma.container [ diff --git a/src/Client/index.html b/src/Client/index.html index c550747b..db90cd02 100644 --- a/src/Client/index.html +++ b/src/Client/index.html @@ -4,7 +4,6 @@ <!-- The "Edge" mode tells IE to use the best available mode; thus IE11 should use IE11 mode. --> <!--<meta http-equiv="X-UA-Compatible" content="IE=edge" />--> <title>Swate - @@ -15,8 +14,8 @@ """) |> ignore - // //newScript.AppendLine("""""") |> ignore - // //newScript.AppendLine("""""") |> ignore - // newScript.AppendLine("""
""") |> ignore - // newScript.AppendLine("") |> ignore - // newScript.ToString() - - ///// Converts a CyGraph to it HTML representation. The div layer has a default size of 600 if not specified otherwise. - //let toCytoHTML (cy:Cytoscape) = - // let guid = Guid.NewGuid().ToString() - // let id = sprintf "e%s" <| Guid.NewGuid().ToString().Replace("-","").Substring(0,10) - // cy.container <- PlainJsonString id - - // let userZoomingEnabled = - // match cy.TryGetTypedValue "zoom" with - // | Some z -> - // match z.TryGetTypedValue "zoomingEnabled" with - // | Some t -> t - // | None -> false - // | None -> false - // |> string - // |> fun s -> s.ToLower() - - // let strCANVAS = // DynamicObj.DynObj.tryGetValue cy "Dims" //tryGetLayoutSize gChart - // match cy.TryGetTypedValue "Canvas" with - // |Some c -> c - // |None -> Canvas.InitDefault() - // //|> fun c -> c?display <- "block" ; c - // |> fun c -> - // c.GetProperties(true) - // |> Seq.map (fun k -> sprintf "%s: %O" k.Key k.Value) - // |> String.concat "; " - // |> sprintf "{ %s }" - - // DynamicObj.DynObj.remove cy "Canvas" - - // /// Create asp.net core able settings - // let settings = - // let converter = PlainJsonStringConverter() :> JsonConverter - // let l = System.Collections.Immutable.ImmutableList.Create(converter) - // let n = new JsonSerializerSettings() - // n.ReferenceLoopHandling <- ReferenceLoopHandling.Serialize - // n.Converters <- l - // n - - // let jsonGraph = JsonConvert.SerializeObject (cy,settings) - - // let html = - // graphDoc - // .Replace("[CANVAS]", strCANVAS) - // .Replace("[ID]", id) - // .Replace("[ZOOMING]", userZoomingEnabled) - // .Replace("[SCRIPTID]", guid.Replace("-","")) - // .Replace("[GRAPHDATA]", jsonGraph) - // html - - ///// Converts a CyGraph to it HTML representation and embeds it into a html page. - //let toEmbeddedHTML (cy:Cytoscape) = - // let graph = - // toCytoHTML cy - // doc - // .Replace("[GRAPH]", graph) - // //.Replace("[DESCRIPTION]", "") diff --git a/src/Server/Database/Template.fs b/src/Server/Database/Template.fs index 9c025ea9..ecec9883 100644 --- a/src/Server/Database/Template.fs +++ b/src/Server/Database/Template.fs @@ -3,10 +3,7 @@ module Database.Template open Neo4j.Driver open System -open Shared.TemplateTypes open Helper - -open ISADotNet open Newtonsoft.Json //type Author = { diff --git a/src/Server/Database/Term.fs b/src/Server/Database/Term.fs index 7f21f6bf..f1581c61 100644 --- a/src/Server/Database/Term.fs +++ b/src/Server/Database/Term.fs @@ -45,12 +45,14 @@ type Queries = static member NameQueryFullText (nodeName: string, ?ontologyFilter: AnyOfOntology, ?limit: int) = let sb = new StringBuilder() - let limit = defaultArg limit 10 - sb.AppendLine $"""CALL db.index.fulltext.queryNodes("TermName",$Name, {{limit: $Limit}}) + sb.AppendLine $"""CALL db.index.fulltext.queryNodes("TermName",$Name) YIELD {nodeName}""" |> ignore if ontologyFilter.IsSome then sb.AppendLine(Queries.OntologyFilter(ontologyFilter.Value, nodeName)) |> ignore sb.AppendLine(Queries.TermReturn nodeName) |> ignore + sb.AppendLine($"""ORDER BY apoc.text.distance(toLower({nodeName}.name), toLower($Name))""") |> ignore + if limit.IsSome then + sb.AppendLine(Queries.Limit(limit.Value)) |> ignore sb.ToString() @@ -74,19 +76,18 @@ type Term(?credentials:Neo4JCredentials, ?session:IAsyncSession) = /// Searchtype defaults to "get term suggestions with auto complete". member this.getByName(termName:string, ?searchType:FullTextSearch, ?sourceOntologyName:AnyOfOntology, ?limit: int) = - let limit = defaultArg limit 5 let nodeName = "node" let fulltextSearchStr = if searchType.IsSome then searchType.Value.ofQueryString termName else FullTextSearch.PerformanceComplete.ofQueryString termName - let query = Queries.NameQueryFullText (nodeName, ?ontologyFilter=sourceOntologyName, limit=limit) + let query = Queries.NameQueryFullText (nodeName, ?ontologyFilter=sourceOntologyName, ?limit = limit) let parameters = Map [ "Name",fulltextSearchStr |> box if sourceOntologyName.IsSome then sourceOntologyName.Value.toParamTuple - "Limit", box limit + if limit.IsSome then "Limit", box limit.Value ] |> Some Neo4j.runQuery(query,parameters,(Term.asTerm(nodeName)),?session=session,?credentials=credentials) @@ -95,10 +96,7 @@ type Term(?credentials:Neo4JCredentials, ?session:IAsyncSession) = /// member this.searchByParentStepwise(query: string, parentId: string, ?searchType:FullTextSearch, ?limit: int) = let limit = defaultArg limit 5 - let searchNameQuery = - """CALL db.index.fulltext.queryNodes("TermName", $Search, {limit: $Limit}) - YIELD node - RETURN node.accession""" + let searchNameQuery = Queries.NameQueryFullText ("node", limit=limit) let searchTreeQuery = """MATCH (node:Term) WHERE node.accession IN $AccessionList @@ -127,7 +125,7 @@ type Term(?credentials:Neo4JCredentials, ?session:IAsyncSession) = session.RunAsync( searchNameQuery, System.Collections.Generic.Dictionary([ - KeyValuePair("Search", box fulltextSearchStr); + KeyValuePair("Name", box fulltextSearchStr); // KeyValuePair("Accession", box parentId); KeyValuePair("Limit", box limit) ]), diff --git a/src/Server/Export.fs b/src/Server/Export.fs deleted file mode 100644 index f893d81c..00000000 --- a/src/Server/Export.fs +++ /dev/null @@ -1,68 +0,0 @@ -module Export - -open System -open ISADotNet -open Shared.OfficeInteropTypes - -//type Column with -// member this.toMatrixElement() = -// let header = this.Header.SwateColumnHeader -// this.Cells -// |> Array.map (fun cell -> -// (cell.Index, header), Option.defaultValue "" cell.Value -// ) -// member this.toMatrixElement(rebaseIndex:int) = -// let header = this.Header.SwateColumnHeader -// this.Cells -// |> Array.map (fun cell -> -// (cell.Index-rebaseIndex, header), Option.defaultValue "" cell.Value -// ) - -//let parseBuildingBlockToMatrix (buildingBlocks:BuildingBlock []) = -// let matrixHeaders = -// buildingBlocks -// |> Array.collect (fun bb -> [| -// bb.MainColumn.Header.SwateColumnHeader -// if bb.hasUnit then -// bb.Unit.Value.Header.SwateColumnHeader -// if bb.hasCompleteTSRTAN then -// bb.TSR.Value.Header.SwateColumnHeader -// bb.TAN.Value.Header.SwateColumnHeader -// |]) -// let rebaseindex = -// let getCellIndices (cellArr:Cell []) = cellArr |> Array.map (fun c -> c.Index) -// buildingBlocks -// |> Array.collect (fun bb -> [| -// yield! bb.MainColumn.Cells |> getCellIndices -// if bb.hasUnit then -// yield! bb.Unit.Value.Cells |> getCellIndices -// if bb.hasCompleteTSRTAN then -// yield! bb.TSR.Value.Cells |> getCellIndices -// yield! bb.TAN.Value.Cells |> getCellIndices -// |]) |> Array.min -// let matrixArr = -// buildingBlocks -// |> Array.collect (fun bb -> [| -// yield! bb.MainColumn.toMatrixElement(rebaseindex) -// if bb.hasUnit then -// yield! bb.Unit.Value.toMatrixElement(rebaseindex) -// if bb.hasCompleteTSRTAN then -// yield! bb.TSR.Value.toMatrixElement(rebaseindex) -// yield! bb.TAN.Value.toMatrixElement(rebaseindex) -// |]) -// let matrix = Collections.Generic.Dictionary<(int*string),string>(Map.ofArray matrixArr) -// matrixHeaders, matrix - -//let parseBuildingBlockToAssay (templateName:string) (buildingBlocks:BuildingBlock []) = -// let matrixHeaders, matrix = parseBuildingBlockToMatrix buildingBlocks -// //printfn "%A" matrixHeaders // contains "Component [instrument model]" -// ISADotNet.XLSX.AssayFile.Assay.fromSparseMatrix templateName matrixHeaders matrix - -//let parseBuildingBlockSeqsToAssay (worksheetNameBuildingBlocks: (string*BuildingBlock []) []) = -// let matrices = -// worksheetNameBuildingBlocks -// |> Array.map (fun (templateName, buildingBlocks) -> -// let matrixHeaders, matrix = parseBuildingBlockToMatrix buildingBlocks -// templateName, Seq.ofArray matrixHeaders, matrix -// ) -// ISADotNet.XLSX.AssayFile.Assay.fromSparseMatrices matrices diff --git a/src/Server/ISADotNet.fs b/src/Server/ISADotNet.fs deleted file mode 100644 index 6a088f6f..00000000 --- a/src/Server/ISADotNet.fs +++ /dev/null @@ -1,282 +0,0 @@ -module ISADotNet - -//open Shared -//open OfficeInteropTypes -//open TermTypes -//open ISADotNet -//open ISADotNet.Json - -//module Assay = - -// open FsSpreadsheet.ExcelIO - -// /// Reads an assay from an xlsx spreadsheetdocument -// /// -// /// As factors and protocols are used for the investigation file, they are returned individually -// /// -// /// The persons from the metadata sheet are returned independently as they are not a part of the assay object -// let fromTemplateSpreadsheet (doc:DocumentFormat.OpenXml.Packaging.SpreadsheetDocument,tableName:string) = - -// let sst = Spreadsheet.tryGetSharedStringTable doc - -// // All sheetnames in the spreadsheetDocument -// let sheetNames = -// Spreadsheet.getWorkbookPart doc -// |> Workbook.get -// |> Sheet.Sheets.get -// |> Sheet.Sheets.getSheets -// |> Seq.map Sheet.getName - -// let assay = -// sheetNames -// |> Seq.tryPick (fun sheetName -> -// Spreadsheet.tryGetWorksheetPartBySheetName sheetName doc -// |> Option.bind (fun wsp -> -// Table.tryGetByNameBy (fun s -> s = tableName) wsp -// |> Option.map (fun table -> -// let sheet = Worksheet.getSheetData wsp.Worksheet -// let headers = Table.getColumnHeaders table -// let m = Table.toSparseValueMatrix sst sheet table -// XLSX.AssayFile.Process.fromSparseMatrix sheetName headers m -// |> fun ps -> Assay.create(ProcessSequence = ps) -// ) -// ) -// ) -// assay - - -///// Only use this function for protocol templates from db -//let rowMajorOfTemplateJson jsonString = -// let assay = Assay.fromString jsonString -// let qAssay = QueryModel.QAssay.fromAssay assay -// if qAssay.Sheets.Length <> 1 then -// failwith "Swate was unable to identify the information from the requested template (). Please open an issue for the developers." -// let template = qAssay.Sheets.Head -// template //QAssay - -//let private ColumnPositionCommentName = "ValueIndex" - -//type OntologyAnnotation with -// member this.toTermMinimal = -// match this.Name, Option.bind Regex.parseTermAccession this.TermAccessionNumber with -// | Some name, Some tan -> TermMinimal.create (AnnotationValue.toString name) tan |> Some -// | Some name, None -> TermMinimal.create (AnnotationValue.toString name) "" |> Some -// | _,_ -> None - -//type ISADotNet.Value with -// member this.toTermMinimal = -// let nameOpt,tanUrlOpt,tsrOpt = ISADotNet.Value.toOptions this -// let name = nameOpt |> Option.defaultValue "" -// let tan = -// if tanUrlOpt.IsSome then -// Regex.parseTermAccession tanUrlOpt.Value |> Option.map (fun tan -> tan.Replace("_",":")) |> Option.defaultValue "" -// else -// "" -// TermMinimal.create name tan - -//let getColumnPosition (oa:OntologyAnnotation) = -// let c = oa.Comments |> Option.map (List.find (fun c -> c.Name = Some ColumnPositionCommentName)) -// c.Value.Value.Value |> int - -//open ISADotNet.QueryModel - -//type ISAValue with - -// member this.toInsertBuildingBlock : (int * InsertBuildingBlock) = -// let buildingBlockType = -// if this.IsFactorValue then -// BuildingBlockType.Factor -// elif this.IsParameterValue then -// BuildingBlockType.Parameter -// elif this.IsCharacteristicValue then -// BuildingBlockType.Characteristic -// elif this.IsComponent then -// BuildingBlockType.Component -// else -// failwithf "This function should only ever be used to parse Factor/Parameter/Characteristic/Component, instead parsed: %A" this -// let colHeaderTermName = -// if this.HasCategory |> not then -// None -// else -// this.Category.toTermMinimal -// let columnPosition = getColumnPosition this.Category -// let unitTerm = if this.HasUnit then this.Unit.toTermMinimal else None -// let headerPrePrint = OfficeInteropTypes.BuildingBlockNamePrePrint.create buildingBlockType colHeaderTermName.Value.Name -// let value = if this.HasValue then Array.singleton this.Value.toTermMinimal else [||] -// //printfn "%A" (if this.HasValue then box this.Value else box "") -// columnPosition, InsertBuildingBlock.create headerPrePrint colHeaderTermName unitTerm value - - -//type IOType with -// member this.toBuildingBlockType = -// match this with -// | Source -> BuildingBlockType.Source -// | Sample -> BuildingBlockType.Sample -// | Data -> BuildingBlockType.Data -// | RawData -> BuildingBlockType.RawDataFile -// | ProcessedData -> BuildingBlockType.DerivedDataFile -// | anyElse -> failwith $"Cannot parse {anyElse} IsaDotNet IOType to BuildingBlockType." - -//let createBuildingBlock_fromProtocolType (protocol: Protocol) = -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create BuildingBlockType.ProtocolType "" -// let columnTerm = Some BuildingBlockType.ProtocolType.getFeaturedColumnTermMinimal -// let rows = -// protocol -// |> fun protType -> -// // row range information is saved in comments and can be accessed + parsed by isadotnet function -// let rowStart, rowEnd = protType.GetRowRange() -// let tmEmpty = TermMinimal.create "" "" -// [| for _ in rowStart .. rowEnd do -// let hasValue = protType.ProtocolType.IsSome -// yield -// if hasValue then protType.ProtocolType.Value.toTermMinimal |> Option.defaultValue tmEmpty else tmEmpty -// |] -// let columnPosition = protocol.ProtocolType |> Option.map getColumnPosition |> Option.defaultValue 0 -// columnPosition, InsertBuildingBlock.create header columnTerm None rows - -///// extend existing ISADotNet.Json.AssayCommonAPI.RowWiseSheet from ISADotNet library with -///// static member to map it to the Swate InsertBuildingBlock type used as input for addBuildingBlock functions -//type QueryModel.QSheet with - -// /// This function is only used for Swate templates. -// /// This function looses values, input and output columns as well as Protocol REF -// member this.headerToInsertBuildingBlockList : InsertBuildingBlock list = -// let headerRow = this.Rows.Head -// if this.Protocols.Length <> 1 then failwith "Protocol template must contain exactly one template" - -// let protocolType = -// let protocol = this.Protocols.Head -// let hasProtocolType = protocol.ProtocolType.IsSome -// if hasProtocolType then -// createBuildingBlock_fromProtocolType protocol |> Some -// else -// None - -// let rawCols = headerRow.Values().Values -// let mutable cols = rawCols |> List.map (fun fv -> fv.toInsertBuildingBlock) -// match protocolType with -// | Some (pbb) -> -// cols <- pbb::cols -// | None -> () -// cols -// |> List.sortBy fst -// |> List.map snd - -// /// This function is the basic parser for all json/xlsx input, with values, input, output and all supported column types. -// member this.toInsertBuildingBlockList : InsertBuildingBlock list = -// let insertBuildingBlockRowList = -// this.Rows |> List.collect (fun r -> -// let cols = -// let cols = r.Values().Values -// cols |> List.map (fun fv -> fv.toInsertBuildingBlock) -// cols |> List.sortBy fst |> List.map snd -// ) -// // Check if protocolREF column exists in assay. because this column is not represented as other columns in isa we need to infer this. -// let protocolRef = -// let sheetName, _ = Process.decomposeName this.SheetName -// let protocolNames = this.Protocols |> List.map (fun x -> x.Name, x) -// // if sheetname and any protocol name differ then we need to create the protocol ref column -// let hasProtocolRef = protocolNames |> List.exists (fun (name, _) -> name.IsSome && name.Value <> sheetName) -// if hasProtocolRef then -// // header must be protocol ref -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create BuildingBlockType.ProtocolREF "" -// let rows = -// this.Protocols -// |> Array.ofList -// |> Array.collect (fun protRef -> -// // row range information is saved in comments and can be accessed + parsed by isadotnet function -// let rowStart, rowEnd = protRef.GetRowRange() -// [| for _ in rowStart .. rowEnd do -// yield TermMinimal.create protRef.Name.Value "" |] -// ) -// InsertBuildingBlock.create header None None rows |> Some -// else -// None - -// let protocolType = -// let hasProtocolType = -// let prots = this.Protocols |> List.choose (fun x -> x.ProtocolType) -// prots.Length > 0 -// if hasProtocolType then -// // header must be protocol type -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create BuildingBlockType.ProtocolType "" -// let columnTerm = Some BuildingBlockType.ProtocolType.getFeaturedColumnTermMinimal -// let rows = -// this.Protocols -// |> Array.ofList -// |> Array.collect (fun protType -> -// // row range information is saved in comments and can be accessed + parsed by isadotnet function -// let rowStart, rowEnd = protType.GetRowRange() -// let tmEmpty = TermMinimal.create "" "" -// [| for _ in rowStart .. rowEnd do -// let hasValue = protType.ProtocolType.IsSome -// yield -// if hasValue then protType.ProtocolType.Value.toTermMinimal |> Option.defaultValue tmEmpty else tmEmpty -// |] -// ) -// InsertBuildingBlock.create header columnTerm None rows |> Some -// else -// None - -// /// https://github.com/nfdi4plants/ISADotNet/issues/80 -// /// This needs to be fixed! For now we only have one input so we can assume "Source" but should this change we need to adapt. -// /// As Soon as this is fixed, create one function for both input and output with (string*IOType option) list as input. -// let input = -// if List.isEmpty this.Inputs then -// None -// else -// let names, types = this.Inputs |> List.unzip -// let inputType = -// //let distinct = (List.choose id >> List.distinct) types -// //try -// // distinct |> List.exactlyOne -// //with -// // | _ -> failwith $"Cannot have input of multiple types: {distinct}" -// IOType.Source.toBuildingBlockType -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create inputType "" -// let rows = names |> List.map (fun x -> TermMinimal.create x "") |> Array.ofList -// InsertBuildingBlock.create header None None rows |> Some - -// /// https://github.com/nfdi4plants/ISADotNet/issues/80 -// let output = -// if List.isEmpty this.Outputs then -// None -// else -// let names, types = this.Outputs |> List.unzip -// //printfn "[OUTPUTS]: %A" this.Outputs -// //printfn "[OUTUT_TYPES]: %A" types -// let inputType = -// // let distinct = (List.choose id >> List.distinct) types -// // try -// // distinct |> List.exactlyOne -// // with -// // | _ -> failwith $"Cannot have input of multiple types: {distinct}" -// // |> fun d -> d.toBuildingBlockType -// IOType.Sample.toBuildingBlockType -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create inputType "" -// let rows = names |> List.map (fun x -> TermMinimal.create x "") |> Array.ofList -// InsertBuildingBlock.create header None None rows |> Some - -// // group building block values by "InsertBuildingBlock" information (column information without values) -// insertBuildingBlockRowList -// |> List.groupBy (fun buildingBlock -> -// buildingBlock.ColumnHeader,buildingBlock.ColumnTerm,buildingBlock.UnitTerm -// ) -// |> List.map (fun ((header,term,unit),buildingBlocks) -> -// let rows = buildingBlocks |> Array.ofList |> Array.collect (fun bb -> bb.Rows) -// InsertBuildingBlock.create header term unit rows -// ) -// |> fun l -> // add special columns -// match protocolRef, protocolType with -// | None, None -> l -// | Some ref, Some t -> ref::t::l -// | Some ref, None -> ref::l -// | None, Some t -> t::l -// |> fun l -> // add input -// match input with -// | Some i -> i::l -// | None -> l -// |> fun l -> // add output -// match output with -// | Some o -> l@[o] -// | None -> l \ No newline at end of file diff --git a/src/Server/Import.fs b/src/Server/Import.fs deleted file mode 100644 index c51ea394..00000000 --- a/src/Server/Import.fs +++ /dev/null @@ -1,46 +0,0 @@ -module Import - -//open ISADotNet -//open ISADotNet.Json - - -/////Input is json string. -//module Json = - -// // The following functions are used to unify all jsonInput to the table type. - -// let fromAssay jsonString = -// let assay = Assay.fromString jsonString -// let tables = QueryModel.QAssay.fromAssay assay -// tables - -// let fromProcessSeq jsonString = -// let processSeq = ProcessSequence.fromString jsonString -// let assay = Assay.create(ProcessSequence = List.ofSeq processSeq) -// let tables = QueryModel.QAssay.fromAssay assay -// tables - -/////Input is byte []. -//module Xlsx = - -// let fromAssay (byteArray: byte []) = -// let ms = new System.IO.MemoryStream(byteArray) -// let _,assay = ISADotNet.XLSX.AssayFile.Assay.fromStream ms -// let tables = QueryModel.QAssay.fromAssay assay -// tables - -///// This function tries to parse any possible json input to building blocks. -//let tryToTable (bytes: byte []) = -// let jsonString = System.Text.Encoding.ASCII.GetString(bytes) -// try -// Json.fromAssay jsonString -// with -// | _ -> -// try -// Json.fromProcessSeq jsonString -// with -// | _ -> -// try -// Xlsx.fromAssay bytes -// with -// | _ -> failwith "Could not match given file to supported file type." \ No newline at end of file diff --git a/src/Server/Properties/launchSettings.json b/src/Server/Properties/launchSettings.json index bcea70db..c957d04c 100644 --- a/src/Server/Properties/launchSettings.json +++ b/src/Server/Properties/launchSettings.json @@ -1,37 +1,12 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iis": { - "applicationUrl": "https://localhost/Server", - "sslPort": 0 - }, - "iisExpress": { - "applicationUrl": "https://localhost/Swate/", - "sslPort": 0 + "profiles": { + "Server": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + } } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS": { - "commandName": "IIS", - "launchUrl": "https://localhost/Swate", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Server": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "NEO4J_TEST_ENV": "MYOUTPUT" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" - } - } } \ No newline at end of file diff --git a/src/Server/Server.fs b/src/Server/Server.fs index 4ed48790..d697b1d0 100644 --- a/src/Server/Server.fs +++ b/src/Server/Server.fs @@ -13,11 +13,13 @@ open Fable.Remoting.Giraffe open Microsoft.Extensions.Logging open Microsoft.Extensions.Configuration -let serviceApi = { +open ARCtrl +open ARCtrl.Json + +let serviceApi: IServiceAPIv1 = { getAppVersion = fun () -> async { return System.AssemblyVersionInformation.AssemblyMetadata_Version } } -open ISADotNet open Microsoft.AspNetCore.Http let dagApiv1 = { @@ -169,20 +171,22 @@ let isaDotNetCommonAPIv1 : IISADotNetCommonAPIv1 = open Database -let templateApi credentials = { - getTemplates = fun () -> async { - let! templates = ARCtrl.Template.Web.getTemplates None - let templatesJson = templates |> Array.map (ARCtrl.Template.Json.Template.toJsonString 0) - return templatesJson - } +let templateApi credentials = + let templateUrl = @"https://github.com/nfdi4plants/Swate-templates/releases/download/latest/templates_v2.0.0.json" + { + getTemplates = fun () -> async { + let! templates = ARCtrl.Template.Web.getTemplates (None) + let templatesJson = ARCtrl.Json.Templates.toJsonString 0 (Array.ofSeq templates) + return templatesJson + } - getTemplateById = fun id -> async { - let! templates = ARCtrl.Template.Web.getTemplates None - let template = templates |> Array.find (fun t -> t.Id = System.Guid(id)) - let templateJson = ARCtrl.Template.Json.Template.toJsonString 0 template - return templateJson + getTemplateById = fun id -> async { + let! templates = ARCtrl.Template.Web.getTemplates (None) + let template = templates |> Seq.find (fun t -> t.Id = System.Guid(id)) + let templateJson = Template.toCompressedJsonString 0 template + return templateJson + } } -} let testApi (ctx: HttpContext): ITestAPI = { test = fun () -> async { @@ -368,10 +372,10 @@ let config (app:IApplicationBuilder) = ) let app = application { - //url "http://localhost:5000" //"http://localhost:5000/" - //app_config config + url "http://*:5000" + app_config config use_router topLevelRouter - //use_cors "CORS_CONFIG" cors_config + use_cors "CORS_CONFIG" cors_config memory_cache use_static "public" use_gzip diff --git a/src/Server/Server.fsproj b/src/Server/Server.fsproj index 13c8bca1..c5564071 100644 --- a/src/Server/Server.fsproj +++ b/src/Server/Server.fsproj @@ -8,16 +8,11 @@ - - - - - @@ -26,5 +21,8 @@ + + + \ No newline at end of file diff --git a/src/Server/Server.fsproj.user b/src/Server/Server.fsproj.user deleted file mode 100644 index f34b3fce..00000000 --- a/src/Server/Server.fsproj.user +++ /dev/null @@ -1,10 +0,0 @@ - - - - IIS - false - - - ProjectDebugger - - \ No newline at end of file diff --git a/src/Server/TemplateMetadata.fs b/src/Server/TemplateMetadata.fs deleted file mode 100644 index 73de4ae3..00000000 --- a/src/Server/TemplateMetadata.fs +++ /dev/null @@ -1,149 +0,0 @@ -module TemplateMetadata - -//open Newtonsoft.Json.Schema - -//let resolver = JSchemaUrlResolver() - -//let jsonSchemaPath = @"public/TemplateMetadataSchema.json" - -//let writeSettings = -// let s = JSchemaWriterSettings() -// s.ReferenceHandling <- JSchemaWriterReferenceHandling.Never -// s - -//let getJsonSchemaAsXml = -// System.IO.File.ReadAllText jsonSchemaPath -// |> fun x -> JSchema.Parse(x,resolver).ToString(writeSettings) - -//open System -//open System.IO -//open FsSpreadsheet.ExcelIO - -//open Shared -//open TemplateTypes -//open DynamicObj -//open Newtonsoft.Json - -//type TemplateMetadataJsonExport() = -// inherit DynamicObj() - -// static member init(?props) = -// let t = TemplateMetadataJsonExport() -// if props.IsSome then -// props.Value |> List.iter t.setProp -// t -// else -// t.setProp("", None) -// t - -// member this.setProp(key,value) = DynObj.setValueOpt this key value - -// member this.print() = DynObj.print this - -// member this.toJson() = this |> JsonConvert.SerializeObject - -//let private maxColumnByRows (rows:DocumentFormat.OpenXml.Spreadsheet.Row []) = -// rows -// |> Array.map (fun row -> (Row.Spans.toBoundaries >> snd) row.Spans) -// |> (Array.max >> int) - -//let private findRowByKey (key:string) (rowValues: string option [][])= -// rowValues -// |> Array.find (fun row -> -// row.[0].IsSome && row.[0].Value = key -// ) - -//let private findRowValuesByKey (key:string) (rowValues: string option [][])= -// findRowByKey key rowValues -// |> Array.tail - -///// Also gets rows from children -//let private getAllRelatedRowsValues (metadata:Metadata.MetadataField) (rows: string option [][]) = -// let rec collectRows (crMetadata:Metadata.MetadataField) = -// if crMetadata.Children.IsEmpty then -// let row = rows |> findRowValuesByKey crMetadata.ExtendedNameKey -// [|row|] -// else -// let childRows = crMetadata.Children |> Array.ofList |> Array.collect (fun child -> collectRows child) -// childRows -// collectRows metadata - -//let private convertToDynObject (sheetData:DocumentFormat.OpenXml.Spreadsheet.SheetData) sst (metadata:Metadata.MetadataField) = -// let rows = SheetData.getRows sheetData |> Array.ofSeq -// let rowValues = -// rows -// |> Array.mapi (fun i row -> -// let spans = row.Spans -// let leftB,rightB = Row.Spans.toBoundaries spans -// [| -// for i = leftB to rightB do -// yield -// Row.tryGetValueAt sst i row -// |] -// ) -// let rec convertDynamic (listIndex: int option) (forwardOutput:TemplateMetadataJsonExport option) (metadata:Metadata.MetadataField) = -// let output = -// if forwardOutput.IsSome then -// forwardOutput.Value -// else -// TemplateMetadataJsonExport.init() -// match metadata with -// /// hit leaves without children -// | isOutput when metadata.Children = [] -> -// let isList = listIndex.IsSome -// let rowValues = rowValues |> findRowValuesByKey metadata.ExtendedNameKey |> Array.map (fun x -> if x.IsSome then x.Value else "") //|> Array.choose id -// if isList then -// let v = -// let tryFromArr = Array.tryItem (listIndex.Value) rowValues -// Option.defaultValue "" tryFromArr -// //if v <> "" then -// output.setProp(metadata.Key, Some v) -// else -// let v = if Array.isEmpty >> not <| rowValues then rowValues.[0] else "" -// //if v <> "" then -// output.setProp(metadata.Key, Some v) -// output -// /// Treat nested lists as object, as nested lists cannot be represented in excel -// | isNestedObjectList when metadata.Children <> [] && metadata.List && metadata.Key <> "" && listIndex.IsSome -> -// /// "WARNING: Cannot parse nested list: metadata.Key, metadata.ExtendedNameKey. Treat by default as object." -// let noList = { metadata with List = false } -// convertDynamic listIndex forwardOutput noList -// /// children are represented by columns -// | isObjectList when metadata.Children <> [] && metadata.List && metadata.Key <> "" -> -// let childRows = getAllRelatedRowsValues metadata rowValues -// /// Only calculate max columns if cell still contains a value. Filter out None and "" -// let notEmptyChildRows = childRows |> Array.map (Array.choose id) |> Array.map (Array.filter (fun x -> x <> "")) -// let maxColumn = notEmptyChildRows |> Array.map Array.length |> Array.max -// let childObjects = -// [| for i = 0 to maxColumn-1 do -// let childOutput = TemplateMetadataJsonExport.init() -// let addChildObject = metadata.Children |> List.map (convertDynamic (Some i) (Some childOutput)) -// yield -// childOutput -// |] -// output.setProp(metadata.Key, Some childObjects) -// output -// /// hit if json object -// | isObject when metadata.Children <> [] && metadata.Key <> "" -> -// let childOutput = TemplateMetadataJsonExport.init() -// // Add key values from children to childOutput. childOutput will be added to output afterwards. -// let childObject = metadata.Children |> List.map (convertDynamic listIndex (Some childOutput)) -// output.setProp(metadata.Key, Some childOutput) -// output -// /// This hits only root objects without key -// | isRoot -> -// let addChildObject = -// metadata.Children -// |> List.map (fun childMetadata -> -// convertDynamic listIndex (Some output) childMetadata -// ) -// output -// convertDynamic None None metadata - -//let parseDynMetadataFromByteArr (byteArray:byte []) = -// let ms = new MemoryStream(byteArray) -// let spreadsheet = Spreadsheet.fromStream ms false -// let sst = Spreadsheet.tryGetSharedStringTable spreadsheet -// let sheetOpt = Spreadsheet.tryGetSheetBySheetName TemplateTypes.Metadata.TemplateMetadataWorksheetName spreadsheet -// if sheetOpt.IsNone then failwith $"Could not find template metadata worksheet: {TemplateTypes.Metadata.TemplateMetadataWorksheetName}" -// convertToDynObject sheetOpt.Value sst Metadata.root diff --git a/src/Server/Version.fs b/src/Server/Version.fs index 80a6293a..4bd2e3df 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "1.0.0-alpha.02" - let [] AssemblyMetadata_ReleaseDate = "24.01.2024" + let [] AssemblyMetadata_Version = "v1.0.0-beta.04" + let [] AssemblyMetadata_ReleaseDate = "28.06.2024" diff --git a/src/Server/public/TemplateMetadataSchema.json b/src/Server/public/TemplateMetadataSchema.json deleted file mode 100644 index 0abda3db..00000000 --- a/src/Server/public/TemplateMetadataSchema.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://raw.githubusercontent.com/nfdi4plants/SWATE_templates/FormattingDocs/templates/TemplatingSchema.json", - "title": "Template Metadata Schema", - "description": "The schema regarding Swate templating-related JSON files.", - "type": "object", - "properties": { - "templateId": { - "description": "The unique identifier of this template. It will be auto generated.", - "type": "string" - }, - "name": { - "description": "The name of the Swate template.", - "type": "string" - }, - "version": { - "description": "The current version of this template in SemVer notation.", - "type": "string" - }, - "description": { - "description": "The description of this template. Use few sentences for succinctness.", - "type": "string" - }, - "docslink": { - "description": "The URL to the documentation page.", - "type": "string" - }, - "organisation": { - "description": "The name of the template associated organisation.", - "type": "string" - }, - "table": { - "description": "The name of the Swate annotation table in the worksheet of the template's excel file.", - "type": "string" - }, - "er": { - "description": "A list of all ERs (endpoint repositories) targeted with this template. ERs are realized as Terms: ", - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/ISA-tools/isa-api/master/isatools/resources/schemas/isa_model_version_1_0_schemas/core/ontology_annotation_schema.json" - }, - "minItems": 1, - "uniqueItems": true - }, - "tags": { - "description": "A list of all tags associated with this template. Tags are realized as Terms: ", - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/ISA-tools/isa-api/master/isatools/resources/schemas/isa_model_version_1_0_schemas/core/ontology_annotation_schema.json" - }, - "minItems": 1, - "uniqueItems": true - }, - "authors": { - "description": "The author(s) of this template.", - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/ISA-tools/isa-api/master/isatools/resources/schemas/isa_model_version_1_0_schemas/core/person_schema.json" - }, - "minItems": 1, - "uniqueItems": true - } - }, - "required": [ - "name", - "version", - "description", - "docslink", - "organisation", - "table", - "er", - "tags", - "authors" - ] -} \ No newline at end of file diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index b05637a8..d557c0ac 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -1,17 +1,25 @@ namespace Shared -open ARCtrl.ISA +open ARCtrl open TermTypes +open System.Collections.Generic /// This module contains helper functions which might be useful for ARCtrl [] module ARCtrlHelper = + [] + type ArcFilesDiscriminate = + | Assay + | Study + | Investigation + | Template + type ArcFiles = - | Template of ARCtrl.Template.Template - | Investigation of ArcInvestigation - | Study of ArcStudy * ArcAssay list - | Assay of ArcAssay + | Template of Template + | Investigation of ArcInvestigation + | Study of ArcStudy * ArcAssay list + | Assay of ArcAssay with member this.Tables() : ArcTables = @@ -21,25 +29,146 @@ module ARCtrlHelper = | Study (s,_) -> s | Assay a -> a + [] + type JsonExportFormat = + | ARCtrl + | ARCtrlCompressed + | ISA + | ROCrate + + static member fromString (str: string) = + match str.ToLower() with + | "arctrl" -> ARCtrl + | "arctrlcompressed" -> ARCtrlCompressed + | "isa" -> ISA + | "rocrate" -> ROCrate + | _ -> failwithf "Unknown JSON export format: %s" str + +module Table = + + /// + /// This functions returns a **copy** of `toJoinTable` without any column already in `activeTable`. + /// + /// + /// + let distinctByHeader (activeTable: ArcTable) (toJoinTable: ArcTable) : ArcTable = + // Remove existing columns + let mutable columnsToRemove = [] + // find duplicate columns + let tablecopy = toJoinTable.Copy() + for header in activeTable.Headers do + let containsAtIndex = tablecopy.Headers |> Seq.tryFindIndex (fun h -> h = header) + if containsAtIndex.IsSome then + columnsToRemove <- containsAtIndex.Value::columnsToRemove + tablecopy.RemoveColumns (Array.ofList columnsToRemove) + tablecopy + + /// + /// This function is meant to prepare a table for joining with another table. + /// + /// It removes columns that are already present in the active table. + /// It removes all values from the new table. + /// It also fills new Input/Output columns with the input/output values of the active table. + /// + /// The output of this function can be used with the SpreadsheetInterface.JoinTable Message. + /// + /// The active/current table + /// The new table, which will be added to the existing one. + let selectiveTablePrepare (activeTable: ArcTable) (toJoinTable: ArcTable) : ArcTable = + // Remove existing columns + let mutable columnsToRemove = [] + // find duplicate columns + let tablecopy = toJoinTable.Copy() + for header in activeTable.Headers do + let containsAtIndex = tablecopy.Headers |> Seq.tryFindIndex (fun h -> h = header) + if containsAtIndex.IsSome then + columnsToRemove <- containsAtIndex.Value::columnsToRemove + tablecopy.RemoveColumns (Array.ofList columnsToRemove) + tablecopy.IteriColumns(fun i c0 -> + let c1 = {c0 with Cells = [||]} + let c2 = + if c1.Header.isInput then + match activeTable.TryGetInputColumn() with + | Some ic -> + {c1 with Cells = ic.Cells} + | _ -> c1 + elif c1.Header.isOutput then + match activeTable.TryGetOutputColumn() with + | Some oc -> + {c1 with Cells = oc.Cells} + | _ -> c1 + else + c1 + tablecopy.UpdateColumn(i, c2.Header, c2.Cells) + ) + tablecopy + +module Helper = + + let arrayMoveColumn (currentColumnIndex: int) (newColumnIndex: int) (arr: ResizeArray<'A>) = + let ele = arr.[currentColumnIndex] + arr.RemoveAt(currentColumnIndex) + arr.Insert(newColumnIndex, ele) + + let dictMoveColumn (currentColumnIndex: int) (newColumnIndex: int) (table: Dictionary) = + /// This is necessary to always access the correct value for an index. + /// It is possible to only copy the specific target column at "currentColumnIndex" and sort the keys in the for loop depending on "currentColumnIndex" and "newColumnIndex". + /// this means. If currentColumnIndex < newColumnIndex then Seq.sortByDescending keys else Seq.sortBy keys. + /// this implementation would result in performance increase, but readability would decrease a lot. + let backupTable = Dictionary(table) + let range = [System.Math.Min(currentColumnIndex, newColumnIndex) .. System.Math.Max(currentColumnIndex,newColumnIndex)] + for columnIndex, rowIndex in backupTable.Keys do + let value = backupTable.[(columnIndex,rowIndex)] + let newColumnIndex = + if columnIndex = currentColumnIndex then + newColumnIndex + elif List.contains columnIndex range then + let modifier = if currentColumnIndex < newColumnIndex then -1 else +1 + let moveTo = modifier + columnIndex + moveTo + else + 0 + columnIndex + let updatedKey = (newColumnIndex, rowIndex) + table.[updatedKey] <- value + [] module Extensions = open ARCtrl.Template + open ArcTableAux + + type OntologyAnnotation with + static member empty() = OntologyAnnotation.create() + static member fromTerm (term:Term) = OntologyAnnotation(term.Name, term.FK_Ontology, term.Accession) + member this.ToTermMinimal() = TermMinimal.create this.NameText this.TermAccessionShort + + type ArcTable with + member this.SetCellAt(columnIndex: int, rowIndex: int, cell: CompositeCell) = + SanityChecks.validateColumn <| CompositeColumn.create(this.Headers.[columnIndex],[|cell|]) + Unchecked.setCellAt(columnIndex, rowIndex,cell) this.Values + Unchecked.fillMissingCells this.Headers this.Values + + member this.SetCellsAt (cells: ((int*int)*CompositeCell) []) = + let columns = cells |> Array.groupBy (fun (index, cell) -> fst index) + for columnIndex, items in columns do + SanityChecks.validateColumn <| CompositeColumn.create(this.Headers.[columnIndex], items |> Array.map snd) + for index, cell in cells do + Unchecked.setCellAt(fst index, snd index, cell) this.Values + Unchecked.fillMissingCells this.Headers this.Values + + member this.MoveColumn(currentIndex: int, nextIndex: int) = + let updateHeaders = + Helper.arrayMoveColumn currentIndex nextIndex this.Headers + let updateBody = + Helper.dictMoveColumn currentIndex nextIndex this.Values + () + type Template with member this.FileName with get() = this.Name.Replace(" ","_") + ".xlsx" type CompositeHeader with - member this.AsButtonName = - match this with - | CompositeHeader.Parameter _ -> "Parameter" - | CompositeHeader.Characteristic _ -> "Characteristic" - | CompositeHeader.Component _ -> "Component" - | CompositeHeader.Factor _ -> "Factor" - | CompositeHeader.Input _ -> "Input" - | CompositeHeader.Output _ -> "Output" - | anyElse -> anyElse.ToString() member this.UpdateWithOA (oa: OntologyAnnotation) = match this with @@ -49,10 +178,10 @@ module Extensions = | CompositeHeader.Factor _ -> CompositeHeader.Factor oa | _ -> failwithf "Cannot update OntologyAnnotation on CompositeHeader without OntologyAnnotation: '%A'" this - static member ParameterEmpty = CompositeHeader.Parameter OntologyAnnotation.empty - static member CharacteristicEmpty = CompositeHeader.Characteristic OntologyAnnotation.empty - static member ComponentEmpty = CompositeHeader.Component OntologyAnnotation.empty - static member FactorEmpty = CompositeHeader.Factor OntologyAnnotation.empty + static member ParameterEmpty = CompositeHeader.Parameter <| OntologyAnnotation.empty() + static member CharacteristicEmpty = CompositeHeader.Characteristic <| OntologyAnnotation.empty() + static member ComponentEmpty = CompositeHeader.Component <| OntologyAnnotation.empty() + static member FactorEmpty = CompositeHeader.Factor <| OntologyAnnotation.empty() static member InputEmpty = CompositeHeader.Input <| IOType.FreeText "" static member OutputEmpty = CompositeHeader.Output <| IOType.FreeText "" @@ -83,7 +212,48 @@ module Extensions = | CompositeHeader.Factor oa -> Some oa | _ -> None + let internal tryFromContent' (content: string []) = + match content with + | [|freetext|] -> CompositeCell.createFreeText freetext |> Ok + | [|name; tsr; tan|] -> CompositeCell.createTermFromString(name, tsr, tan) |> Ok + | [|value; name; tsr; tan|] -> CompositeCell.createUnitizedFromString(value, name, tsr, tan) |> Ok + | anyElse -> sprintf "Unable to convert \"%A\" to CompositeCell." anyElse |> Error + type CompositeCell with + + static member tryFromContent (content: string []) = + match tryFromContent' content with + | Ok r -> Some r + | Error _ -> None + + static member fromContent (content: string []) = + match tryFromContent' content with + | Ok r -> r + | Error msg -> raise (exn msg) + + member this.ToTabStr() = this.GetContent() |> String.concat "\t" + + static member fromTabStr (str:string) = + let content = str.Split('\t', System.StringSplitOptions.TrimEntries) + CompositeCell.fromContent content + + static member ToTabTxt (cells: CompositeCell []) = + cells + |> Array.map (fun c -> c.ToTabStr()) + |> String.concat (System.Environment.NewLine) + + static member fromTabTxt (tabTxt: string) = + let lines = tabTxt.Split(System.Environment.NewLine, System.StringSplitOptions.None) + let cells = lines |> Array.map (fun line -> CompositeCell.fromTabStr line) + cells + + member this.ConvertToValidCell (header: CompositeHeader) = + match header.IsTermColumn, this with + | true, CompositeCell.Term _ | true, CompositeCell.Unitized _ -> this + | true, CompositeCell.FreeText txt -> this.ToTermCell() + | false, CompositeCell.Term _ | false, CompositeCell.Unitized _ -> this.ToFreeTextCell() + | false, CompositeCell.FreeText _ -> this + member this.UpdateWithOA(oa:OntologyAnnotation) = match this with | CompositeCell.Term _ -> CompositeCell.createTerm oa @@ -94,11 +264,13 @@ module Extensions = match this with | CompositeCell.Term oa -> oa | CompositeCell.Unitized (v, oa) -> oa - | CompositeCell.FreeText t -> OntologyAnnotation.fromString t + | CompositeCell.FreeText t -> OntologyAnnotation.create t member this.UpdateMainField(s: string) = match this with - | CompositeCell.Term oa -> CompositeCell.Term ({oa with Name = Some s}) + | CompositeCell.Term oa -> + oa.Name <- Some s + CompositeCell.Term oa | CompositeCell.Unitized (_, oa) -> CompositeCell.Unitized (s, oa) | CompositeCell.FreeText _ -> CompositeCell.FreeText s @@ -107,7 +279,7 @@ module Extensions = /// /// member this.UpdateTSR(tsr: string) = - let updateTSR (oa: OntologyAnnotation) = {oa with TermSourceREF = tsr |> Some} + let updateTSR (oa: OntologyAnnotation) = oa.TermSourceREF <- Some tsr ;oa match this with | CompositeCell.Term oa -> CompositeCell.Term (updateTSR oa) | CompositeCell.Unitized (v, oa) -> CompositeCell.Unitized (v, updateTSR oa) @@ -118,12 +290,8 @@ module Extensions = /// /// member this.UpdateTAN(tan: string) = - let updateTAN (oa: OntologyAnnotation) = {oa with TermAccessionNumber = tan |> Some} + let updateTAN (oa: OntologyAnnotation) = oa.TermSourceREF <- Some tan ;oa match this with | CompositeCell.Term oa -> CompositeCell.Term (updateTAN oa) | CompositeCell.Unitized (v, oa) -> CompositeCell.Unitized (v, updateTAN oa) | _ -> this - - type OntologyAnnotation with - static member fromTerm (term:Term) = OntologyAnnotation.fromString(term.Name, term.FK_Ontology, term.Accession) - member this.ToTermMinimal() = TermMinimal.create this.NameText this.TermAccessionShort diff --git a/src/Shared/OfficeInteropTypes.fs b/src/Shared/OfficeInteropTypes.fs index 82b0f100..de433843 100644 --- a/src/Shared/OfficeInteropTypes.fs +++ b/src/Shared/OfficeInteropTypes.fs @@ -1,7 +1,7 @@ namespace Shared open System -open ARCtrl.ISA +open ARCtrl module OfficeInteropTypes = diff --git a/src/Shared/Regex.fs b/src/Shared/Regex.fs index a1e4bd88..7fbaf42d 100644 --- a/src/Shared/Regex.fs +++ b/src/Shared/Regex.fs @@ -1,5 +1,6 @@ namespace Shared +[] module Regex = module Pattern = diff --git a/src/Shared/Shared.fs b/src/Shared/Shared.fs index 141e7894..e1832539 100644 --- a/src/Shared/Shared.fs +++ b/src/Shared/Shared.fs @@ -3,7 +3,6 @@ namespace Shared open System open Shared open TermTypes -open TemplateTypes module Route = @@ -33,18 +32,6 @@ module SorensenDice = calculateDistance resultSet searchSet ) -///This type is still used for JsonExporter page. -[] -type JsonExportType = -| ProcessSeq -| Assay -| ProtocolTemplate - member this.toExplanation = - match this with - | ProcessSeq -> "Sequence of ISA process.json." - | Assay -> "ISA assay.json" - | ProtocolTemplate -> "Schema for Swate protocol template, with template metadata and table json." - /// Development api type ITestAPI = { test : unit -> Async @@ -152,7 +139,7 @@ type IOntologyAPIv3 = { type ITemplateAPIv1 = { // must return template as string, fable remoting cannot do conversion automatically - getTemplates : unit -> Async + getTemplates : unit -> Async getTemplateById : string -> Async } diff --git a/src/Shared/Shared.fsproj b/src/Shared/Shared.fsproj index fe91584b..25d70a89 100644 --- a/src/Shared/Shared.fsproj +++ b/src/Shared/Shared.fsproj @@ -4,6 +4,7 @@ net8.0 + @@ -11,7 +12,6 @@ - diff --git a/src/Shared/StaticTermCollection.fs b/src/Shared/StaticTermCollection.fs new file mode 100644 index 00000000..eed1a05b --- /dev/null +++ b/src/Shared/StaticTermCollection.fs @@ -0,0 +1,18 @@ +module Shared.TermCollection + +open ARCtrl + +/// +/// https://github.com/nfdi4plants/nfdi4plants_ontology/issues/85 +/// +let Published = OntologyAnnotation("published","EFO","EFO:0001796") + +/// +/// https://github.com/nfdi4plants/Swate/issues/409#issuecomment-2176134201 +/// +let PublicationStatus = OntologyAnnotation("publication status","EFO","EFO:0001742") + +/// +/// https://github.com/nfdi4plants/Swate/issues/409#issuecomment-2176134201 +/// +let PersonRoleWithinExperiment = OntologyAnnotation("person role within the experiment","AGRO","AGRO:00000378") \ No newline at end of file diff --git a/src/Shared/TemplateTypes.fs b/src/Shared/TemplateTypes.fs deleted file mode 100644 index f0829b94..00000000 --- a/src/Shared/TemplateTypes.fs +++ /dev/null @@ -1,153 +0,0 @@ -namespace Shared - -module TemplateTypes = - - open System - - module Metadata = - - [] - let TemplateMetadataWorksheetName = "SwateTemplateMetadata" - - type MetadataField = { - /// Will be used to create rowKey - Key : string - ExtendedNameKey : string - Description : string option - List : bool - Children : MetadataField list - } with - static member create(key,?extKey,?desc,?islist,?children) = { - Key = key - ExtendedNameKey = if extKey.IsSome then extKey.Value else "" - Description = desc - List = if islist.IsSome then islist.Value else false - Children = if children.IsSome then children.Value else [] - } - - static member createParentKey parentKey (newKey:string) = - let nk = newKey.Replace("#","") - $"{parentKey} {nk}".Trim() - - /// Loop through all children to create ExtendedNameKey for all MetaDataField types - member this.extendedNameKeys = - let rec extendName (parentKey:string) (metadata:MetadataField) = - let nextParentKey = MetadataField.createParentKey parentKey metadata.Key - { metadata with - ExtendedNameKey = nextParentKey - Children = if metadata.Children.IsEmpty |> not then metadata.Children |> List.map (extendName nextParentKey) else metadata.Children - } - extendName "" this - - static member createWithExtendedKeys(key,?extKey,?desc,?islist,?children) = - { - Key = key - ExtendedNameKey = if extKey.IsSome then extKey.Value else "" - Description = desc - List = if islist.IsSome then islist.Value else false - Children = if children.IsSome then children.Value else [] - }.extendedNameKeys - - - module RowKeys = - [] - let DescriptionKey = "Description" - [] - let TemplateIdKey = "Id" - - open RowKeys - - // annotation value - let private tsr = MetadataField.create("Term Source REF") - let private tan = MetadataField.create("Term Accession Number") - let private annotationValue = MetadataField.create("#") - // - let private id = MetadataField.create(TemplateIdKey, desc ="The unique identifier of this template. It will be auto generated.") - let private name = MetadataField.create("Name", desc="The name of the Swate template.") - let private version = MetadataField.create("Version", desc="The current version of this template in SemVer notation.") - let private description = MetadataField.create(DescriptionKey, desc ="The description of this template. Use few sentences for succinctness.") - //let private docslink = MetadataField.create("Docslink", desc="The URL to the documentation page.") - let private organisation = MetadataField.create("Organisation", desc="""The name of the template associated organisation. "DataPLANT" will trigger the "DataPLANT" batch of honor for the template.""") - let private table = MetadataField.create("Table", desc="The name of the Swate annotation table in the workbook of the template's excel file.") - // er - let private er = MetadataField.create("ER",desc="A list of all ERs (endpoint repositories) targeted with this template. ERs are realized as Terms.",islist=true, children = [annotationValue; tan; tsr]) - // tags - let private tags = MetadataField.create("Tags",desc="A list of all tags associated with this template. Tags are realized as Terms.", islist=true, children = [annotationValue; tan; tsr] ) - // person - let private lastName = MetadataField.create("Last Name") - let private firstName = MetadataField.create("First Name") - let private midIntiials = MetadataField.create("Mid Initials") - let private email = MetadataField.create("Email") - let private phone = MetadataField.create("Phone") - let private fax = MetadataField.create("Fax") - let private address = MetadataField.create("Address") - let private affiliation = MetadataField.create("Affiliation") - let private orcid = MetadataField.create("ORCID") - //let private roles = MetadataField.create("Role", children = [annotationValue; tan; tsr]) - let private roleAnnotationValue = MetadataField.create("Role") - let private roleTAN = MetadataField.create("Role Term Accession Number") - let private roleTSR = MetadataField.create("Role Term Source REF") - let private authors = MetadataField.create("Authors",desc="The author(s) of this template.", islist = true, children = [lastName; firstName; midIntiials; email; phone; fax; address; affiliation; orcid; roleAnnotationValue; roleTAN; roleTSR]) - // entry - let root = MetadataField.createWithExtendedKeys("",children=[id;name;version;description;(*docslink;*)organisation;table;er;tags;authors]) - - type Template = { - Id : string - Name : string - Description : string - Organisation : string - Version : string - Authors : string - /// endpoint repository tags - Er_Tags : string [] - Tags : string [] - TemplateBuildingBlocks : OfficeInteropTypes.InsertBuildingBlock list - LastUpdated : System.DateTime - // WIP - Used : int - Rating : int - } with - static member create id name describtion organisation version lastUpdated author ertags tags templateBuildingBlocks used rating = { - Id = id - Name = name - Description = describtion - Organisation = organisation - Version = version - Authors = author - Er_Tags = ertags - Tags = tags - TemplateBuildingBlocks = templateBuildingBlocks - LastUpdated = lastUpdated - Used = used - // WIP - Rating = rating - } - - type Organisation = - | DataPLANT - | Other of string - - type Author = { - LastName: string - FirstName: string - MidInitials: string - Email: string - Phone: string - Fax: string - Adress: string - Affiliation: string - ORCID: string - Role: TermTypes.TermMinimal - } - - type TemplateForm = { - Id : System.Guid - Name : string - Version : string - Description : string - Organisation : Organisation - Table : string - ER_List : TermTypes.TermMinimal [] - Tags : TermTypes.TermMinimal [] - Authors : Author [] - } \ No newline at end of file diff --git a/src/Shared/TermTypes.fs b/src/Shared/TermTypes.fs index e94b7d98..27b5e130 100644 --- a/src/Shared/TermTypes.fs +++ b/src/Shared/TermTypes.fs @@ -1,10 +1,9 @@ namespace Shared -open ARCtrl.ISA +open ARCtrl module TermTypes = - open Shared.Regex open System type Ontology = { diff --git a/tests/Server/JsonExport.Tests.fs b/tests/Server/JsonExport.Tests.fs index 704743e3..2795d75f 100644 --- a/tests/Server/JsonExport.Tests.fs +++ b/tests/Server/JsonExport.Tests.fs @@ -4,7 +4,6 @@ open Expecto open Shared open Server -open ISADotNet open JsonImport open System.IO diff --git a/tests/Server/JsonImport.Tests.fs b/tests/Server/JsonImport.Tests.fs index 4deb61bc..c5b057ab 100644 --- a/tests/Server/JsonImport.Tests.fs +++ b/tests/Server/JsonImport.Tests.fs @@ -4,7 +4,6 @@ open Expecto open Shared open Server -open ISADotNet let x = 2 //open JsonImport