From bad74066b10159a5aa3537077c4a3ff7d2e2ecbb Mon Sep 17 00:00:00 2001 From: dotasek Date: Wed, 1 Nov 2023 12:14:26 -0400 Subject: [PATCH] Add additional features for common validation options (#149) * WIP presets dropdown * WIP ips au preset * Rename extension, ig, and profile state * Add update code for extensions * Add profiles to presets, add all required presets * Make pretty * Check IPS codes, add bundle validation rules to presets * Use i18n for presets * Add options checkbox for IPS codes * Rename re-used extension display * WIP add bundle validation rule widget, fix some session id update logic * Layout tweaks * Adjust font * Add help text for bundle validation, layout improvements * Refactor file upload buttons * Add preset widget to upload page * Adjust preset widget layout and description --- gradle.properties | 2 +- src/commonMain/kotlin/constants/Extensions.kt | 3 + .../kotlin/model/BundleValidationRule.kt | 9 + src/commonMain/kotlin/model/CliContext.kt | 8 + .../static-content/polyglot/de_DE.json | 24 +- .../resources/static-content/polyglot/en.json | 24 +- .../resources/static-content/polyglot/es.json | 24 +- src/jsMain/kotlin/App.kt | 4 +- src/jsMain/kotlin/Main.kt | 15 +- .../kotlin/model/BundleValidationRule.kt | 57 ++++ src/jsMain/kotlin/model/CliContext.kt | 24 +- .../reactredux/containers/FileUploadTab.kt | 26 +- .../reactredux/containers/ManualEntryTab.kt | 23 ++ .../reactredux/containers/OptionsPage.kt | 37 ++- .../slices/ValidationContextSlice.kt | 36 ++- .../ui/components/buttons/ImageButton.kt | 4 +- .../header/LanguageOption/LanguageSelect.kt | 5 +- .../options/AddBundleValidationRule.kt | 265 ++++++++++++++++++ .../ui/components/options/AddExtension.kt | 28 +- .../ui/components/options/AddProfile.kt | 23 +- .../options/BundleValidationRuleDisplay.kt | 83 ++++++ .../ui/components/options/OptionsPage.kt | 98 +++++-- .../ui/components/options/PresetSelect.kt | 139 +++++++++ .../{ExtensionDisplay.kt => UrlDisplay.kt} | 16 +- .../components/options/menu/TextFieldEntry.kt | 8 +- .../tabs/entrytab/ManualEntryTab.kt | 65 ++++- ...ryButtonBar.kt => ManualValidateButton.kt} | 8 +- .../tabs/uploadtab/FileUploadButton.kt | 53 ++++ .../tabs/uploadtab/FileUploadButtonBar.kt | 101 ------- .../tabs/uploadtab/FileUploadTab.kt | 73 ++++- .../tabs/uploadtab/FileValidateButton.kt | 68 +++++ src/jsMain/kotlin/utils/Language.kt | 16 +- src/jsMain/kotlin/utils/Preset.kt | 142 ++++++++++ .../kotlin/model/BundleValidationRule.kt | 3 + 34 files changed, 1263 insertions(+), 251 deletions(-) create mode 100644 src/commonMain/kotlin/constants/Extensions.kt create mode 100644 src/commonMain/kotlin/model/BundleValidationRule.kt create mode 100644 src/jsMain/kotlin/model/BundleValidationRule.kt create mode 100644 src/jsMain/kotlin/ui/components/options/AddBundleValidationRule.kt create mode 100644 src/jsMain/kotlin/ui/components/options/BundleValidationRuleDisplay.kt create mode 100644 src/jsMain/kotlin/ui/components/options/PresetSelect.kt rename src/jsMain/kotlin/ui/components/options/{ExtensionDisplay.kt => UrlDisplay.kt} (76%) rename src/jsMain/kotlin/ui/components/tabs/entrytab/{ManualEntryButtonBar.kt => ManualValidateButton.kt} (86%) create mode 100644 src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadButton.kt delete mode 100644 src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadButtonBar.kt create mode 100644 src/jsMain/kotlin/ui/components/tabs/uploadtab/FileValidateButton.kt create mode 100644 src/jsMain/kotlin/utils/Preset.kt create mode 100644 src/jvmMain/kotlin/model/BundleValidationRule.kt diff --git a/gradle.properties b/gradle.properties index dbb02a26..211df645 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ kotlin.code.style=official kotlin.js.generate.executable.default=false # versions -fhirCoreVersion=6.1.12 +fhirCoreVersion=6.2.1 junitVersion=5.7.1 mockk_version=1.10.2 diff --git a/src/commonMain/kotlin/constants/Extensions.kt b/src/commonMain/kotlin/constants/Extensions.kt new file mode 100644 index 00000000..a680a3e3 --- /dev/null +++ b/src/commonMain/kotlin/constants/Extensions.kt @@ -0,0 +1,3 @@ +package constants + +const val ANY_EXTENSION = "any" \ No newline at end of file diff --git a/src/commonMain/kotlin/model/BundleValidationRule.kt b/src/commonMain/kotlin/model/BundleValidationRule.kt new file mode 100644 index 00000000..29a558c5 --- /dev/null +++ b/src/commonMain/kotlin/model/BundleValidationRule.kt @@ -0,0 +1,9 @@ +package model + +expect class BundleValidationRule() { + fun getRule(): String + fun setRule(rule: String): BundleValidationRule + + fun getProfile(): String + fun setProfile(profile: String): BundleValidationRule +} \ No newline at end of file diff --git a/src/commonMain/kotlin/model/CliContext.kt b/src/commonMain/kotlin/model/CliContext.kt index 96bb797a..2b149694 100644 --- a/src/commonMain/kotlin/model/CliContext.kt +++ b/src/commonMain/kotlin/model/CliContext.kt @@ -54,4 +54,12 @@ expect class CliContext() { fun setExtensions(extensions: List) : CliContext fun getExtensions() : List + + fun setCheckIPSCodes(checkIPSCodes : Boolean) : CliContext + + fun isCheckIPSCodes() : Boolean + + fun setBundleValidationRules(bundleValidationRules: List) : CliContext + + fun getBundleValidationRules():List } \ No newline at end of file diff --git a/src/commonMain/resources/static-content/polyglot/de_DE.json b/src/commonMain/resources/static-content/polyglot/de_DE.json index 6b0f1dc1..27c0f635 100644 --- a/src/commonMain/resources/static-content/polyglot/de_DE.json +++ b/src/commonMain/resources/static-content/polyglot/de_DE.json @@ -27,8 +27,10 @@ "options_flags_binding_warnings_description" : "Wenn der Validator auf einen Code stößt, der nicht Teil eines extensible Bindings ist, fügt er eine Warnung hinzu, um vorzuschlagen, dass der Code überprüft werden sollte. Der Validator kann nicht feststellen, ob die Bedeutung des Codes ihn zu einer unangemessenen Erweiterung macht oder nicht; dies erfordert eine menschliche Überprüfung. Daher auch die Warnung. Aber der Code kann gültig sein - deshalb ist extensible definiert - also ist es in einigen Anwendungen des Validators angebracht, diese Warnungen auszuschalten.", "options_flags_show_times_title" : "Show Times (showTimes)", "options_flags_show_times_description" : "Wenn Sie diese Option wählen, gibt der Validator in der Ausgabe eine Zeile aus, in der er zusammenfasst, wie lange einige interne Prozesse gedauert haben.", - "options_flags_allow_example_title" : "Erlaube Beispiel URLs (allow-example-urls)", + "options_flags_allow_example_title" : "Erlaube Beispiel URLs (-allow-example-urls)", "options_flags_allow_example_description" : "Einige der Beispiele in der FHIR-Spezifikation enthalten URLs, die auf example.org verweisen. Standardmäßig markiert der Validator solche Verweise immer als Fehler, aber dies kann mit diesem Parameter überschrieben werden.", + "options_flags_check_ips_codes_title" : "Check IPS Codes (-check-ips-codes) -German", + "options_flags_check_ips_codes_description" : "When selected, the validator will report a list of SNOMED CT codes used by the source(s) being validated that are not part of the SNOMED CT IPS free set (as hints). -German", "options_fhir_title" : "FHIR version", "options_default_label" : "Version", "options_fhir_description" : "Der Validator prüft die Ressource anhand der Basisspezifikation. Standardmäßig ist dies die Spezifikation Version 4.0.1.", @@ -53,6 +55,17 @@ "options_profiles_description" : "The canonical URLs for the profiles you wish to validate against. These are usually clearly specified on the page where the profile is published.-German", "options_profiles_not_added" : "Added Profiles (%{addedProfiles})-German", "options_profiles_added" : "Added Profiles (%{addedProfiles}):-German", + "options_bundle_validation_rules_title" : "Bundle Validation-German", + "options_bundle_validation_rules_description" : "The validator can validate a particular resource in the bundle against a given profile. This is separate fro m the '-profile' option, which is the profile for the bundle itself. Each entry consists of a rule definition and a profile. The rule is defined as a resource name, an integer index, or both. Rule examples:-German", + "options_bundle_validation_rules_example_1" : "Patient - validate any patient against the nominated profile-German", + "options_bundle_validation_rules_example_2" : "0 - validate the first resource (index is 0 based) against the nominated profile-German", + "options_bundle_validation_rules_example_3" : "Patient:0 - validate the first patient resource against the nominated profile-German", + "options_bundle_validation_rules_not_added" : "Added Rules (%{addedProfiles})-German", + "options_bundle_validation_rules_added" : "Added Rules (%{addedProfiles}):-German", + "options_bundle_validation_rules_rule_label" : "Rule-German", + "options_bundle_validation_rules_rule_description" : "A Resource name, an integer index, or both.-German", + "options_bundle_validation_rules_profile_label" : "Profile-German", + "options_bundle_validation_rules_profile_description" : "The nominated profile, by canonical URL-German", "options_settings_title" : "Sonstige Einstellungen", "options_settings_snomed_title" : "Wählen Sie die SNOMED Version aus", "options_settings_snomed_description" : "Sie können angeben, welche Ausgabe von SNOMED CT der Terminologieserver bei der SNOMED CT-Validierung verwenden soll.", @@ -72,5 +85,14 @@ "validation_errors" : "Fehler", "validation_warnings" : "Warnungen", "validation_info" : "Information", + "preset_label" : "Common Validation Options... -German", + "preset_description" : "Click to automatically set options for common validation tasks-German", + "preset_notification" : "Set to validate using %{selectedPreset}. Select the Options tab for more settings. -German", + "preset_fhir_resource" : "FHIR Resource -German", + "preset_ips" : "IPS Document -German", + "preset_ips_au" : "Australian IPS Document -German", + "present_cda" : "CDA Document -German", + "preset_us_ccda" : "US CCDA Document -German", + "preset_sql_view": "SQL View Definition -German", "language" : "Sprache" } \ No newline at end of file diff --git a/src/commonMain/resources/static-content/polyglot/en.json b/src/commonMain/resources/static-content/polyglot/en.json index bf8d6b1c..4a18b1d5 100644 --- a/src/commonMain/resources/static-content/polyglot/en.json +++ b/src/commonMain/resources/static-content/polyglot/en.json @@ -27,8 +27,10 @@ "options_flags_binding_warnings_description" : "When the validator encounters a code that is not part of an extensible binding, it adds a warning to suggest that the code be reviewed. The validator can't determine whether the meaning of the code makes it an inappropriate extension, or not; this requires human review. Hence, the warning. But the code may be valid - that's why extensible is defined - so in some operational uses of the validator, it is appropriate to turn these warnings off", "options_flags_show_times_title" : "Show Times (-show-times)", "options_flags_show_times_description" : "When selected the validator will produce a line in the output summarizing how long some internal processes took.", - "options_flags_allow_example_title" : "Allow Example URLs (allow-example-urls)", + "options_flags_allow_example_title" : "Allow Example URLs (-allow-example-urls)", "options_flags_allow_example_description" : "Some of the examples in the FHIR specification have URLs in them that refer to example.org. By default, the validator will always mark any such references as an error, but this can be overridden with this parameter.", + "options_flags_check_ips_codes_title" : "Check IPS Codes (-check-ips-codes)", + "options_flags_check_ips_codes_description" : "When selected, the validator will report a list of SNOMED CT codes used by the source(s) being validated that are not part of the SNOMED CT IPS free set (as hints).", "options_fhir_title" : "FHIR version", "options_default_label" : "Version", "options_fhir_description" : "The validator checks the resource against the base specification. By default, this is specification version 4.0.1.", @@ -53,6 +55,17 @@ "options_profiles_description" : "The canonical URLs for the profiles you wish to validate against. These are usually clearly specified on the page where the profile is published.", "options_profiles_not_added" : "Added Profiles (%{addedProfiles})", "options_profiles_added" : "Added Profiles (%{addedProfiles}):", + "options_bundle_validation_rules_title" : "Bundle Validation", + "options_bundle_validation_rules_description" : "The validator can validate a particular resource in the bundle against a given profile. This is separate from the '-profile' option, which is the profile for the bundle itself. Each entry consists of a rule definition and a profile. The rule is defined as a resource name, an integer index, or both. Rule examples:", + "options_bundle_validation_rules_example_1" : "Patient - validate any patient against the nominated profile", + "options_bundle_validation_rules_example_2" : "0 - validate the first resource (index is 0 based) against the nominated profile", + "options_bundle_validation_rules_example_3" : "Patient:0 - validate the first patient resource against the nominated profile", + "options_bundle_validation_rules_not_added" : "Added Rules (%{addedProfiles})", + "options_bundle_validation_rules_added" : "Added Rules (%{addedProfiles}):", + "options_bundle_validation_rules_rule_label" : "Rule", + "options_bundle_validation_rules_rule_description" : "A Resource name, an integer index, or both.", + "options_bundle_validation_rules_profile_label" : "Profile", + "options_bundle_validation_rules_profile_description" : "The nominated profile, by canonical URL", "options_settings_title" : "Other Settings", "options_settings_snomed_title" : "Select SNOMED Version", "options_settings_snomed_description" : "You can specify which edition of SNOMED CT for the terminology server to use when doing SNOMED CT Validation.", @@ -72,5 +85,14 @@ "validation_errors" : "Errors", "validation_warnings" : "Warnings", "validation_info" : "Information", + "preset_label" : "Common Validation Options...", + "preset_description" : "Click to automatically set options for common validation tasks", + "preset_notification" : "Set to validate using %{selectedPreset}. Select the Options tab for more settings.", + "preset_fhir_resource" : "FHIR Resource", + "preset_ips" : "IPS Document", + "preset_ips_au" : "Australian IPS Document", + "present_cda" : "CDA Document", + "preset_us_ccda" : "US CCDA Document", + "preset_sql_view": "SQL View Definition", "language" : "Language" } \ No newline at end of file diff --git a/src/commonMain/resources/static-content/polyglot/es.json b/src/commonMain/resources/static-content/polyglot/es.json index c9997ba9..d800548b 100644 --- a/src/commonMain/resources/static-content/polyglot/es.json +++ b/src/commonMain/resources/static-content/polyglot/es.json @@ -27,8 +27,10 @@ "options_flags_binding_warnings_description": "Cuando el validador encuentra un código que no es parte de una vinculacíon terminológica extensible, agrega una alerta para sugerir que el código sea revisado. El validador no puede determinar si el significado del código genera una extensión inapropiada o no; esto requiere revisión manual. De allí proviene la alerta - Pero el código puede ser válido - para eso se define como extensible - asi que en algunos usos operacionales del validador, es correcto apagar algunos de estas alertas", "options_flags_show_times_title": "Mostrar Tiempos (-show-times)", "options_flags_show_times_description": "Cuando se selecciona esta opción el validador generará una linea en la salida resumiendo cuanto tiempo demoran los procesos internos", - "options_flags_allow_example_title": "Permitir URLs de ejemplo (allow-example-urls)", + "options_flags_allow_example_title": "Permitir URLs de ejemplo (-allow-example-urls)", "options_flags_allow_example_description": "Alguno de los ejemplos en la especificación FHIR tienen URLs que apuntan a example.org. Por omisión, el validador las marcará como un error pero esta conducta puede ser modificado a través de este parámetro", + "options_flags_check_ips_codes_title" : "Check IPS Codes (-check-ips-codes) -Spanish", + "options_flags_check_ips_codes_description" : "When selected, the validator will report a list of SNOMED CT codes used by the source(s) being validated that are not part of the SNOMED CT IPS free set (as hints). -Spanish", "options_fhir_title": "Versión de FHIR", "options_default_label": "Version", "options_fhir_description": "El validador verifica el recurso contra la especificación base. Por omisión, esta es la especificación versión 4.0.1", @@ -54,6 +56,17 @@ "options_profiles_description" : "The canonical URLs for the profiles you wish to validate against. These are usually clearly specified on the page where the profile is published.-Spanish", "options_profiles_not_added" : "Added Profiles (%{addedProfiles})-Spanish", "options_profiles_added" : "Added Profiles (%{addedProfiles}):-Spanish", + "options_bundle_validation_rules_title" : "Bundle Validation-Spanish", + "options_bundle_validation_rules_description" : "The validator can validate a particular resource in the bundle against a given profile. This is separate fro m the '-profile' option, which is the profile for the bundle itself. Each entry consists of a rule definition and a profile. The rule is defined as a resource name, an integer index, or both. Rule examples:-Spanish", + "options_bundle_validation_rules_example_1" : "Patient - validate any patient against the nominated profile-Spanish", + "options_bundle_validation_rules_example_2" : "0 - validate the first resource (index is 0 based) against the nominated profile-Spanish", + "options_bundle_validation_rules_example_3" : "Patient:0 - validate the first patient resource against the nominated profile-Spanish", + "options_bundle_validation_rules_not_added" : "Added Rules (%{addedProfiles})-Spanish", + "options_bundle_validation_rules_added" : "Added Rules (%{addedProfiles}):-Spanish", + "options_bundle_validation_rules_rule_label" : "Rule-Spanish", + "options_bundle_validation_rules_rule_description" : "A Resource name, an integer index, or both.-Spanish", + "options_bundle_validation_rules_profile_label" : "Profile-Spanish", + "options_bundle_validation_rules_profile_description" : "The nominated profile, by canonical URL-Spanish", "options_settings_snomed_title": "Elegir versión de SNOMED", "options_settings_snomed_description": "Puede especificar qué edición de SNOMED CT utilizará para el servidor terminológico cuando valida SNOMED CT", "options_settings_tm_title": "Definir Servidor de Terminología", @@ -72,5 +85,14 @@ "validation_errors": "Errores", "validation_warnings": "Alertas", "validation_info": "Información", + "preset_label" : "Common Validation Options... -Spanish", + "preset_description" : "Click to automatically set options for common validation tasks-Spanish", + "preset_notification" : "Set to validate using %{selectedPreset}. Select the Options tab for more settings. -Spanish", + "preset_fhir_resource" : "FHIR Resource -Spanish", + "preset_ips" : "IPS Document -Spanish", + "preset_ips_au" : "Australian IPS Document -Spanish", + "present_cda" : "CDA Document -Spanish", + "preset_us_ccda" : "US CCDA Document -Spanish", + "preset_sql_view": "SQL View Definition -Spanish", "language": "Lenguaje" } \ No newline at end of file diff --git a/src/jsMain/kotlin/App.kt b/src/jsMain/kotlin/App.kt index b428d788..bc7578e2 100644 --- a/src/jsMain/kotlin/App.kt +++ b/src/jsMain/kotlin/App.kt @@ -14,7 +14,7 @@ import ui.components.tabs.tabLayout import kotlinx.browser.window import model.CliContext import utils.Language -import utils.getSelectedLanguage + external interface AppProps : Props { var appScreen: AppScreen @@ -32,7 +32,7 @@ val mainScope = MainScope() fun languageSetup(props: AppProps) { for (item in window.navigator.languages) { val prefix = item.substring(0, 2) - var selectedLanguage = getSelectedLanguage(prefix) + var selectedLanguage = Language.getSelectedLanguage(prefix) if (selectedLanguage != null) { props.setLanguage(selectedLanguage) props.fetchPolyglot(selectedLanguage.getLanguageCode()); diff --git a/src/jsMain/kotlin/Main.kt b/src/jsMain/kotlin/Main.kt index 15e85f1f..35b9305a 100644 --- a/src/jsMain/kotlin/Main.kt +++ b/src/jsMain/kotlin/Main.kt @@ -1,9 +1,13 @@ import css.GlobalStyles import kotlinx.browser.document +import react.create +import react.dom.client.createRoot import react.dom.render +import react.redux.Provider import react.redux.provider import reactredux.containers.app +import reactredux.store.createStore import reactredux.store.myStore fun main() { @@ -15,11 +19,10 @@ fun main() { * This is where we dynamically add all generated ui elements. */ - val rootDiv = document.getElementById("root")!! - render(rootDiv){ - provider(myStore) { - app { } - } - } + val container = document.getElementById("root")!! + createRoot(container).render(Provider.create { // this: {ChildrenBuilder & Props & ProviderProps} + store = myStore // Set the store. Because it is named ProviderProps.store, you can't use name 'store' for your store any more. + app {} + }) } diff --git a/src/jsMain/kotlin/model/BundleValidationRule.kt b/src/jsMain/kotlin/model/BundleValidationRule.kt new file mode 100644 index 00000000..35c7ddb1 --- /dev/null +++ b/src/jsMain/kotlin/model/BundleValidationRule.kt @@ -0,0 +1,57 @@ +package model + +import kotlinx.js.Object +import kotlinx.serialization.Serializable +import utils.Preset + +@Serializable +actual class BundleValidationRule actual constructor() { + + private var rule: String = "" + private var profile: String = "" + + actual fun getRule(): String { + return rule + } + + actual fun setRule(rule: String): BundleValidationRule { + this.rule = rule + return this + } + + actual fun getProfile(): String { + return profile + } + + actual fun setProfile(profile: String): BundleValidationRule { + this.profile = profile + return this + } + + override fun hashCode(): Int { + return toDisplayString(this).hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is BundleValidationRule) { + return false + } else { + return rule.equals(other.getRule()) && profile.equals(other.getProfile()) + } + } + + companion object { + fun toDisplayString(rule:BundleValidationRule): String { + return "${rule.rule} ${rule.profile}" + } + + fun findByDisplayString(displayString : String, collection : Collection) : BundleValidationRule? { + for (rule in collection) { + if (displayString == toDisplayString(rule)) { + return rule + } + } + return null + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/model/CliContext.kt b/src/jsMain/kotlin/model/CliContext.kt index 0a782a05..a8ecb4d2 100644 --- a/src/jsMain/kotlin/model/CliContext.kt +++ b/src/jsMain/kotlin/model/CliContext.kt @@ -25,10 +25,14 @@ actual class CliContext actual constructor() { private var igs: List = listOf() private var profiles: List = listOf() + private var checkIPSCodes = false + + private var bundleValidationRules : List = listOf() + private var locale: String = "" init { sv = "4.0.1" - locale = "de" + locale = "en" } actual fun isDoNative(): Boolean { @@ -215,4 +219,22 @@ actual class CliContext actual constructor() { actual fun getExtensions() : List { return this.extensions } + + actual fun setCheckIPSCodes(checkIPSCodes: Boolean): CliContext { + this.checkIPSCodes = checkIPSCodes; + return this; + } + + actual fun isCheckIPSCodes(): Boolean { + return this.checkIPSCodes + } + + actual fun getBundleValidationRules(): List { + return bundleValidationRules + } + + actual fun setBundleValidationRules(bundleValidationRules: List): CliContext { + this.bundleValidationRules = bundleValidationRules + return this + } } \ No newline at end of file diff --git a/src/jsMain/kotlin/reactredux/containers/FileUploadTab.kt b/src/jsMain/kotlin/reactredux/containers/FileUploadTab.kt index 94302215..807f2f68 100644 --- a/src/jsMain/kotlin/reactredux/containers/FileUploadTab.kt +++ b/src/jsMain/kotlin/reactredux/containers/FileUploadTab.kt @@ -1,14 +1,13 @@ package reactredux.containers import Polyglot -import model.CliContext -import model.FileInfo -import model.ValidationOutcome +import model.* import react.ComponentClass import react.Props import react.invoke import react.redux.rConnect import reactredux.slices.UploadedResourceSlice +import reactredux.slices.ValidationContextSlice import reactredux.slices.ValidationSessionSlice import reactredux.store.AppState import redux.RAction @@ -28,6 +27,12 @@ private interface FileUploadTabDispatchProps : Props { var setSessionId: (String) -> Unit var toggleValidationInProgress: (Boolean, FileInfo) -> Unit var addValidationOutcome: (ValidationOutcome) -> Unit + + var updateCliContext: (CliContext) -> Unit + var updateIgPackageInfoSet: (Set) -> Unit + var updateExtensionSet: (Set) -> Unit + var updateProfileSet: (Set)-> Unit + var updateBundleValidationRuleSet: (Set) -> Unit } val fileUploadTab: ComponentClass = @@ -47,5 +52,20 @@ val fileUploadTab: ComponentClass = fileInfo)) } addValidationOutcome = { dispatch(UploadedResourceSlice.AddValidationOutcome(it)) } + updateCliContext = { + dispatch(ValidationContextSlice.UpdateCliContext(it)) + } + updateIgPackageInfoSet = { + dispatch(ValidationContextSlice.UpdateIgPackageInfoSet(it)) + } + updateExtensionSet = { + dispatch(ValidationContextSlice.UpdateExtensionSet(it)) + } + updateProfileSet = { + dispatch(ValidationContextSlice.UpdateProfileSet(it)) + } + updateBundleValidationRuleSet = { + dispatch(ValidationContextSlice.UpdateBundleValidationRuleSet(it)) + } } )(FileUploadTab::class.js.unsafeCast>()) \ No newline at end of file diff --git a/src/jsMain/kotlin/reactredux/containers/ManualEntryTab.kt b/src/jsMain/kotlin/reactredux/containers/ManualEntryTab.kt index 68fe0798..0919f977 100644 --- a/src/jsMain/kotlin/reactredux/containers/ManualEntryTab.kt +++ b/src/jsMain/kotlin/reactredux/containers/ManualEntryTab.kt @@ -1,13 +1,16 @@ package reactredux.containers import Polyglot +import model.BundleValidationRule import model.CliContext +import model.PackageInfo import model.ValidationOutcome import react.ComponentClass import react.Props import react.invoke import react.redux.rConnect import reactredux.slices.ManualEntrySlice +import reactredux.slices.ValidationContextSlice import reactredux.slices.ValidationSessionSlice import reactredux.store.AppState import redux.RAction @@ -28,6 +31,11 @@ private interface ManualEntryTabDispatchProps : Props { var setValidationOutcome: (ValidationOutcome) -> Unit var toggleValidationInProgress: (Boolean) -> Unit var updateCurrentlyEnteredText: (String) -> Unit + var updateCliContext: (CliContext) -> Unit + var updateIgPackageInfoSet: (Set) -> Unit + var updateExtensionSet: (Set) -> Unit + var updateProfileSet: (Set)-> Unit + var updateBundleValidationRuleSet: (Set) -> Unit var setSessionId: (String) -> Unit } @@ -45,6 +53,21 @@ val manualEntryTab: ComponentClass = setValidationOutcome = { dispatch(ManualEntrySlice.AddManualEntryOutcome(it)) } toggleValidationInProgress = { dispatch(ManualEntrySlice.ToggleValidationInProgress(it)) } updateCurrentlyEnteredText = { dispatch(ManualEntrySlice.UpdateCurrentlyEnteredText(it)) } + updateCliContext = { + dispatch(ValidationContextSlice.UpdateCliContext(it)) + } + updateIgPackageInfoSet = { + dispatch(ValidationContextSlice.UpdateIgPackageInfoSet(it)) + } + updateExtensionSet = { + dispatch(ValidationContextSlice.UpdateExtensionSet(it)) + } + updateProfileSet = { + dispatch(ValidationContextSlice.UpdateProfileSet(it)) + } + updateBundleValidationRuleSet = { + dispatch(ValidationContextSlice.UpdateBundleValidationRuleSet(it)) + } setSessionId = { id: String -> dispatch(ValidationSessionSlice.SetSessionId(id)) } } )(ManualEntryTab::class.js.unsafeCast>()) \ No newline at end of file diff --git a/src/jsMain/kotlin/reactredux/containers/OptionsPage.kt b/src/jsMain/kotlin/reactredux/containers/OptionsPage.kt index 40cdc5ac..28ed9954 100644 --- a/src/jsMain/kotlin/reactredux/containers/OptionsPage.kt +++ b/src/jsMain/kotlin/reactredux/containers/OptionsPage.kt @@ -1,6 +1,7 @@ package reactredux.containers import Polyglot +import model.BundleValidationRule import model.CliContext import model.PackageInfo import react.ComponentClass @@ -17,18 +18,20 @@ import ui.components.options.OptionsPageProps private interface OptionsPageStateProps : Props { var cliContext: CliContext - var selectedIgPackageInfo: Set - var addedExtensionInfo: Set - var addedProfiles: Set + var igPackageInfoSet: Set + var extensionSet: Set + var profileSet: Set + var bundleValidationRuleSet: Set var polyglot: Polyglot } private interface OptionsPageDispatchProps : Props { var updateCliContext: (CliContext) -> Unit - var updateSelectedIgPackageInfo: (Set) -> Unit - var updateAddedExtensionUrl: (Set) -> Unit + var updateIgPackageInfoSet: (Set) -> Unit + var updateExtensionSet: (Set) -> Unit var setSessionId: (String) -> Unit - var updateAddedProfiles: (Set) -> Unit + var updateProfileSet: (Set) -> Unit + var updateBundleValidationRuleSet: (Set) -> Unit } @@ -36,24 +39,28 @@ val optionsPage: ComponentClass = rConnect( { state, _ -> cliContext = state.validationContextSlice.cliContext - selectedIgPackageInfo = state.validationContextSlice.selectedIgPackageInfo - addedExtensionInfo = state.validationContextSlice.addedExtensionInfo - addedProfiles = state.validationContextSlice.addedProfiles + igPackageInfoSet = state.validationContextSlice.igPackageInfoSet + extensionSet = state.validationContextSlice.extensionSet + profileSet = state.validationContextSlice.profileSet + bundleValidationRuleSet = state.validationContextSlice.bundleValidationRuleSet polyglot = state.localizationSlice.polyglotInstance }, { dispatch, _ -> updateCliContext = { dispatch(ValidationContextSlice.UpdateCliContext(it)) } - updateSelectedIgPackageInfo = { - dispatch(ValidationContextSlice.UpdateSelectedIgPackageInfo(it)) + updateIgPackageInfoSet = { + dispatch(ValidationContextSlice.UpdateIgPackageInfoSet(it)) } - updateAddedExtensionUrl = { - dispatch(ValidationContextSlice.UpdateAddedExtensionUrl(it)) + updateExtensionSet = { + dispatch(ValidationContextSlice.UpdateExtensionSet(it)) } setSessionId = { id: String -> dispatch(ValidationSessionSlice.SetSessionId(id)) } - updateAddedProfiles = { - dispatch(ValidationContextSlice.UpdateAddedProfile(it)) + updateProfileSet = { + dispatch(ValidationContextSlice.UpdateProfileSet(it)) + } + updateBundleValidationRuleSet = { + dispatch(ValidationContextSlice.UpdateBundleValidationRuleSet(it)) } } )(OptionsPage::class.js.unsafeCast>()) \ No newline at end of file diff --git a/src/jsMain/kotlin/reactredux/slices/ValidationContextSlice.kt b/src/jsMain/kotlin/reactredux/slices/ValidationContextSlice.kt index 46b3f4d1..100310d6 100644 --- a/src/jsMain/kotlin/reactredux/slices/ValidationContextSlice.kt +++ b/src/jsMain/kotlin/reactredux/slices/ValidationContextSlice.kt @@ -1,5 +1,6 @@ package reactredux.slices +import model.BundleValidationRule import model.CliContext import model.PackageInfo import redux.RAction @@ -7,36 +8,43 @@ import redux.RAction object ValidationContextSlice { data class State( - val selectedIgPackageInfo: Set = mutableSetOf(), - val addedExtensionInfo: Set = mutableSetOf(), - val addedProfiles: Set = mutableSetOf(), + val igPackageInfoSet: Set = mutableSetOf(), + val extensionSet: Set = mutableSetOf(), + val profileSet: Set = mutableSetOf(), + val bundleValidationRuleSet: Set = mutableSetOf(), val cliContext: CliContext = CliContext() ) - data class UpdateSelectedIgPackageInfo(val packageInfo: Set) : RAction + data class UpdateIgPackageInfoSet(val packageInfo: Set) : RAction data class UpdateCliContext(val cliContext: CliContext) : RAction - data class UpdateAddedExtensionUrl(val extensionUrls: Set) : RAction + data class UpdateExtensionSet(val extensionSet: Set) : RAction - data class UpdateAddedProfile(val profiles: Set) : RAction + data class UpdateProfileSet(val profileSet: Set) : RAction + + data class UpdateBundleValidationRuleSet(val bundleValidationRuleSet: Set) : RAction fun reducer(state: State = State(), action: RAction): State { return when (action) { - is UpdateSelectedIgPackageInfo -> state.copy( - selectedIgPackageInfo = action.packageInfo, + is UpdateIgPackageInfoSet -> state.copy( + igPackageInfoSet = action.packageInfo, cliContext = state.cliContext.setIgs(action.packageInfo.map{PackageInfo.igLookupString(it)}.toList()) ) is UpdateCliContext -> state.copy( cliContext = action.cliContext ) - is UpdateAddedExtensionUrl -> state.copy( - addedExtensionInfo = action.extensionUrls, - cliContext = state.cliContext.setExtensions(action.extensionUrls.toList()) + is UpdateExtensionSet -> state.copy( + extensionSet = action.extensionSet, + cliContext = state.cliContext.setExtensions(action.extensionSet.toList()) + ) + is UpdateProfileSet -> state.copy( + profileSet = action.profileSet, + cliContext = state.cliContext.setProfiles(action.profileSet.toList()) ) - is UpdateAddedProfile -> state.copy( - addedProfiles = action.profiles, - cliContext = state.cliContext.setProfiles(action.profiles.toList()) + is UpdateBundleValidationRuleSet -> state.copy( + bundleValidationRuleSet = action.bundleValidationRuleSet, + cliContext = state.cliContext.setBundleValidationRules(action.bundleValidationRuleSet.toList()) ) else -> state } diff --git a/src/jsMain/kotlin/ui/components/buttons/ImageButton.kt b/src/jsMain/kotlin/ui/components/buttons/ImageButton.kt index da0b155f..5446f21a 100644 --- a/src/jsMain/kotlin/ui/components/buttons/ImageButton.kt +++ b/src/jsMain/kotlin/ui/components/buttons/ImageButton.kt @@ -27,7 +27,7 @@ class ImageButton : RComponent() { styledDiv { css { +ImageButtonStyle.button - border(width = 1.px, style = BorderStyle.solid, color = props.borderColor, borderRadius = 5.px) + border(width = 1.px, style = BorderStyle.solid, color = props.borderColor, borderRadius = 4.px) backgroundColor = props.backgroundColor hover { backgroundColor = props.borderColor.changeAlpha(0.1) @@ -84,7 +84,7 @@ object ImageButtonStyle : StyleSheet("ImageButtonStyle", isStatic = true) { cursor = Cursor.pointer display = Display.inlineFlex flexDirection = FlexDirection.row - minHeight = 32.px + minHeight = 36.px alignSelf = Align.center padding(horizontal = 16.px, vertical = 8.px) } diff --git a/src/jsMain/kotlin/ui/components/header/LanguageOption/LanguageSelect.kt b/src/jsMain/kotlin/ui/components/header/LanguageOption/LanguageSelect.kt index 4161a944..e50f19c8 100644 --- a/src/jsMain/kotlin/ui/components/header/LanguageOption/LanguageSelect.kt +++ b/src/jsMain/kotlin/ui/components/header/LanguageOption/LanguageSelect.kt @@ -9,7 +9,7 @@ import mui.system.sx import react.Props import react.ReactNode import utils.Language -import utils.getSelectedLanguage + external interface LanguageSelectProps : Props { var polyglot: Polyglot @@ -18,6 +18,7 @@ external interface LanguageSelectProps : Props { var setLanguage: (Language) -> Unit var cliContext: CliContext var updateCliContext: (CliContext) -> Unit + } class LanguageSelect(props : LanguageSelectProps) : RComponent() { @@ -42,7 +43,7 @@ class LanguageSelect(props : LanguageSelectProps) : RComponent - val selectedLanguage = getSelectedLanguage(event.target.value) + val selectedLanguage = Language.getSelectedLanguage(event.target.value) if (selectedLanguage != null) { props.setLanguage(selectedLanguage) props.fetchPolyglot(selectedLanguage.getLanguageCode()); diff --git a/src/jsMain/kotlin/ui/components/options/AddBundleValidationRule.kt b/src/jsMain/kotlin/ui/components/options/AddBundleValidationRule.kt new file mode 100644 index 00000000..613d2b54 --- /dev/null +++ b/src/jsMain/kotlin/ui/components/options/AddBundleValidationRule.kt @@ -0,0 +1,265 @@ +package ui.components.options + +import Polyglot +import css.const.BORDER_GRAY +import css.const.WHITE +import css.const.SWITCH_GRAY +import css.text.TextStyle +import kotlinx.css.* +import react.* +import ui.components.buttons.imageButton +import kotlinx.browser.document +import kotlinx.css.properties.border +import kotlinx.html.InputType +import kotlinx.html.id +import model.BundleValidationRule +import utils.getJS + +import model.CliContext + +import mui.icons.material.InfoOutlined +import mui.material.Tooltip + +import org.w3c.dom.HTMLInputElement +import react.dom.attrs +import react.dom.defaultValue +import react.dom.li +import react.dom.ul +import styled.* + +external interface AddBundleValidationRuleProps : Props { + var bundleValidationRuleSet : MutableSet + var onUpdateBundleValidationRuleSet : (BundleValidationRule, Boolean) -> Unit + var updateCliContext : (CliContext) -> Unit + var cliContext : CliContext + var polyglot: Polyglot +} + +class AddBundleValidationRuleState : State { +} + +class AddBundleValidationRule : RComponent() { + val ruleInputId = "bundle_validation_rule_entry" + val profileInputId = "bundle_validation_profile_entry" + init { + state = AddProfileState() + } + + override fun RBuilder.render() { + styledDiv { + css { + +AddExtensionStyle.mainDiv + } + styledSpan { + css { + +TextStyle.optionsDetailText + +IgSelectorStyle.title + } + +props.polyglot.t("options_bundle_validation_rules_description") + ul { + li { + +props.polyglot.t("options_bundle_validation_rules_example_1") + } + li { + +props.polyglot.t("options_bundle_validation_rules_example_2") + } + li { + +props.polyglot.t("options_bundle_validation_rules_example_3") + } + } + } + + styledSpan { + css { + display = Display.grid + gridTemplateColumns = GridTemplateColumns(LinearDimension.maxContent, LinearDimension("0.4fr"), LinearDimension.minContent) + gap = LinearDimension("10px") + alignItems = Align.start + } + + styledSpan { + css { + gridColumn = GridColumn("1") + +AddBundleValidationRuleStyle.ruleEntryDetailText + } + +props.polyglot.t("options_bundle_validation_rules_rule_label") + } + + styledInput { + css { + gridColumn = GridColumn("2") + +AddBundleValidationRuleStyle.ruleEntryTextArea + } + attrs { + type = InputType.text + defaultValue = "" + id = ruleInputId + } + } + styledDiv { + css { + gridColumn = GridColumn("3") + + AddBundleValidationRuleStyle.ruleEntryTooltip + } + Tooltip { + attrs { + title = ReactNode( + props.polyglot.t("options_bundle_validation_rules_rule_description") + ) + } + InfoOutlined {} + } + } + styledSpan { + + css { + gridColumn = GridColumn("1") + +AddBundleValidationRuleStyle.ruleEntryDetailText + + } + + props.polyglot.t("options_bundle_validation_rules_profile_label") + + } + styledInput { + css { + gridColumn = GridColumn("2") + +AddBundleValidationRuleStyle.ruleEntryTextArea + } + attrs { + type = InputType.text + defaultValue = "" + id = profileInputId + } + } + styledDiv { + css { + gridColumn = GridColumn("3") + + AddBundleValidationRuleStyle.ruleEntryTooltip + } + Tooltip { + attrs { + title = ReactNode(props.polyglot.t("options_bundle_validation_rules_profile_description")) + } + InfoOutlined {} + } + } + styledSpan { + css { + gridColumn = GridColumn("2 / span 2") + textAlign = TextAlign.end + } + imageButton { + backgroundColor = WHITE + borderColor = SWITCH_GRAY + image = "images/add_circle_black_24dp.svg" + label = props.polyglot.t("options_ig_add") + onSelected = { + val rule = (document.getElementById(ruleInputId) as HTMLInputElement).value + val profile = (document.getElementById(profileInputId) as HTMLInputElement).value + + val bundleValidationRule = BundleValidationRule().setRule(rule).setProfile(profile) + + props.onUpdateBundleValidationRuleSet( + bundleValidationRule, + false + ) + } + } + } + } + styledDiv { + css { + padding(top = 24.px) + +if (props.bundleValidationRuleSet.isEmpty()) TextStyle.optionsDetailText else TextStyle.optionName + } + val polyglotKey = if (props.bundleValidationRuleSet.isEmpty()) { + "options_bundle_validation_rules_not_added" + } else { + "options_bundle_validation_rules_added" + } + +props.polyglot.t( + polyglotKey, + getJS(arrayOf(Pair("addedProfiles", props.bundleValidationRuleSet.size.toString()))) + ) + } + styledDiv { + css { + +IgSelectorStyle.selectedIgsDiv + if (!props.bundleValidationRuleSet.isEmpty()) { + padding(top = 16.px) + } + } + props.bundleValidationRuleSet.forEach { _rule -> + bundleValidationRuleDisplay { + rule = _rule + onDelete = { + props.onUpdateBundleValidationRuleSet(_rule, true) + } + } + } + } + } + } +} + +fun RBuilder.addBundleValidationRule(handler: AddBundleValidationRuleProps.() -> Unit) { + return child(AddBundleValidationRule::class) { + this.attrs(handler) + } +} + +object AddBundleValidationRuleStyle : StyleSheet("AddBundleValidationRuleStyle", isStatic = true) { + val mainDiv by css { + display = Display.flex + flexDirection = FlexDirection.column + padding(horizontal = 8.px) + } + val ruleEntryDetailText by css { + fontFamily = TextStyle.FONT_FAMILY_MAIN + fontSize = 11.pt + fontWeight = FontWeight.w200 + display = Display.inlineBlock + verticalAlign = VerticalAlign.middle + resize = Resize.none + paddingTop = 16.px + } + val ruleEntryTextArea by css { + display = Display.inlineBlock + verticalAlign = VerticalAlign.middle + resize = Resize.none + width = 100.pct + height = 42.px + marginRight = 16.px + outline = Outline.none + border(width = 1.px, color = BORDER_GRAY, style = BorderStyle.solid) + backgroundColor = Color.transparent + justifyContent = JustifyContent.center + +TextStyle.optionsDetailText + } + val ruleEntryTooltip by css { + paddingTop = 8.px + paddingLeft = 8.px + } + val title by css { + paddingBottom = 16.px + } + val selectedRulesDiv by css { + display = Display.flex + flexDirection = FlexDirection.row + flexWrap = FlexWrap.wrap + } +} + + + + + + + + + + + + + + diff --git a/src/jsMain/kotlin/ui/components/options/AddExtension.kt b/src/jsMain/kotlin/ui/components/options/AddExtension.kt index 490c5397..8cb13c61 100644 --- a/src/jsMain/kotlin/ui/components/options/AddExtension.kt +++ b/src/jsMain/kotlin/ui/components/options/AddExtension.kt @@ -12,6 +12,8 @@ import kotlinx.html.InputType import kotlinx.html.id import utils.getJS +import constants.ANY_EXTENSION + import model.CliContext import org.w3c.dom.HTMLInputElement import react.dom.attrs @@ -21,8 +23,8 @@ import ui.components.options.menu.TextFieldEntryStyle import ui.components.options.menu.checkboxWithDetails external interface AddExtensionProps : Props { - var addedExtensionSet : MutableSet - var onUpdateExtension : (String, Boolean) -> Unit + var extensionSet : MutableSet + var onUpdateExtensionSet : (String, Boolean) -> Unit var updateCliContext : (CliContext) -> Unit var cliContext : CliContext var onUpdateAnyExtension : (Boolean) -> Unit @@ -32,6 +34,8 @@ external interface AddExtensionProps : Props { class AddExtensionState : State { } + + class AddExtension : RComponent() { val textInputId = "extension_entry" init { @@ -63,7 +67,7 @@ class AddExtension : RComponent() { if (!anyChecked()) { styledSpan { css { - +TextFieldEntryStyle.textFieldAndGButtonDiv + +TextFieldEntryStyle.textFieldAndAddButtonDiv } styledInput { css { @@ -82,7 +86,7 @@ class AddExtension : RComponent() { image = "images/add_circle_black_24dp.svg" label = props.polyglot.t("options_ig_add") onSelected = { - props.onUpdateExtension((document.getElementById(textInputId) as HTMLInputElement).value, false) + props.onUpdateExtensionSet((document.getElementById(textInputId) as HTMLInputElement).value, false) } } } @@ -90,24 +94,24 @@ class AddExtension : RComponent() { styledDiv { css { padding(top = 24.px) - + if (props.addedExtensionSet.isEmpty()) TextStyle.optionsDetailText else TextStyle.optionName + + if (props.extensionSet.isEmpty()) TextStyle.optionsDetailText else TextStyle.optionName } - val polyglotKey = if (props.addedExtensionSet.isEmpty()) { "options_extensions_not_added"} else { "options_extensions_added"} - +props.polyglot.t(polyglotKey, getJS(arrayOf(Pair("addedExtensions", props.addedExtensionSet.size.toString())))) + val polyglotKey = if (props.extensionSet.isEmpty()) { "options_extensions_not_added"} else { "options_extensions_added"} + +props.polyglot.t(polyglotKey, getJS(arrayOf(Pair("addedExtensions", props.extensionSet.size.toString())))) } styledDiv { css { +IgSelectorStyle.selectedIgsDiv - if (!props.addedExtensionSet.isEmpty()) { + if (!props.extensionSet.isEmpty()) { padding(top = 16.px) } } - props.addedExtensionSet.forEach { _url -> - extensionDisplay { + props.extensionSet.forEach { _url -> + urlDisplay { polyglot = props.polyglot url = _url onDelete = { - props.onUpdateExtension(_url, true) + props.onUpdateExtensionSet(_url, true) } } } @@ -116,7 +120,7 @@ class AddExtension : RComponent() { } } private fun anyChecked() : Boolean { - return props.addedExtensionSet.contains("any") + return props.extensionSet.contains(ANY_EXTENSION) } } diff --git a/src/jsMain/kotlin/ui/components/options/AddProfile.kt b/src/jsMain/kotlin/ui/components/options/AddProfile.kt index 4a170ada..f0c86a0a 100644 --- a/src/jsMain/kotlin/ui/components/options/AddProfile.kt +++ b/src/jsMain/kotlin/ui/components/options/AddProfile.kt @@ -18,11 +18,10 @@ import react.dom.attrs import react.dom.defaultValue import styled.* import ui.components.options.menu.TextFieldEntryStyle -import ui.components.options.menu.checkboxWithDetails external interface AddProfileProps : Props { - var addedProfiles : MutableSet - var onUpdateProfiles : (String, Boolean) -> Unit + var profileSet : MutableSet + var onUpdateProfileSet : (String, Boolean) -> Unit var updateCliContext : (CliContext) -> Unit var cliContext : CliContext var polyglot: Polyglot @@ -53,7 +52,7 @@ class AddProfile : RComponent() { styledSpan { css { - +TextFieldEntryStyle.textFieldAndGButtonDiv + +TextFieldEntryStyle.textFieldAndAddButtonDiv } styledInput { css { @@ -72,7 +71,7 @@ class AddProfile : RComponent() { image = "images/add_circle_black_24dp.svg" label = props.polyglot.t("options_ig_add") onSelected = { - props.onUpdateProfiles( + props.onUpdateProfileSet( (document.getElementById(textInputId) as HTMLInputElement).value, false ) @@ -83,31 +82,31 @@ class AddProfile : RComponent() { styledDiv { css { padding(top = 24.px) - +if (props.addedProfiles.isEmpty()) TextStyle.optionsDetailText else TextStyle.optionName + +if (props.profileSet.isEmpty()) TextStyle.optionsDetailText else TextStyle.optionName } - val polyglotKey = if (props.addedProfiles.isEmpty()) { + val polyglotKey = if (props.profileSet.isEmpty()) { "options_profiles_not_added" } else { "options_profiles_added" } +props.polyglot.t( polyglotKey, - getJS(arrayOf(Pair("addedProfiles", props.addedProfiles.size.toString()))) + getJS(arrayOf(Pair("addedProfiles", props.profileSet.size.toString()))) ) } styledDiv { css { +IgSelectorStyle.selectedIgsDiv - if (!props.addedProfiles.isEmpty()) { + if (!props.profileSet.isEmpty()) { padding(top = 16.px) } } - props.addedProfiles.forEach { _url -> - extensionDisplay { + props.profileSet.forEach { _url -> + urlDisplay { polyglot = props.polyglot url = _url onDelete = { - props.onUpdateProfiles(_url, true) + props.onUpdateProfileSet(_url, true) } } } diff --git a/src/jsMain/kotlin/ui/components/options/BundleValidationRuleDisplay.kt b/src/jsMain/kotlin/ui/components/options/BundleValidationRuleDisplay.kt new file mode 100644 index 00000000..f95a5de2 --- /dev/null +++ b/src/jsMain/kotlin/ui/components/options/BundleValidationRuleDisplay.kt @@ -0,0 +1,83 @@ +package ui.components.options + +import css.const.* +import css.text.TextStyle +import kotlinx.css.* +import kotlinx.css.properties.border +import kotlinx.html.js.onClickFunction +import model.BundleValidationRule +import react.* +import react.dom.attrs +import styled.* + +external interface BundleValidationRuleDisplayProps : Props { + var rule: BundleValidationRule + var onDelete: () -> Unit +} + +class BundleValidationRuleDisplay : RComponent() { + override fun RBuilder.render() { + styledDiv { + css { + +BundleValidationRuleDisplayStyle.mainDiv + } + + styledSpan { + css { + +TextStyle.dropDownLabel + +BundleValidationRuleDisplayStyle.igName + } + +BundleValidationRule.toDisplayString(props.rule) + } + styledImg { + css { + +BundleValidationRuleDisplayStyle.closeButton + } + attrs { + src = "images/close_black.png" + onClickFunction = { + props.onDelete() + } + } + } + } + } +} + +/** + * React Component Builder + */ +fun RBuilder.bundleValidationRuleDisplay(handler: BundleValidationRuleDisplayProps.() -> Unit) { + return child(BundleValidationRuleDisplay::class) { + this.attrs(handler) + } +} + +/** + * CSS + */ +object BundleValidationRuleDisplayStyle : StyleSheet("BundleValidationRuleDisplayStyle", isStatic = true) { + val mainDiv by css { + display = Display.flex + flexDirection = FlexDirection.row + border(width = 1.px, style = BorderStyle.solid, color = BORDER_GRAY) + margin(right = 16.px, top = 4.px, bottom = 4.px) + padding(horizontal = 16.px, vertical = 8.px) + backgroundColor = WHITE + } + val igName by css { + padding(right = 16.px) + } + val closeButton by css { + width = 16.px + height = 16.px + alignSelf = Align.center + borderRadius = 50.pct + hover { + backgroundColor = HIGHLIGHT_GRAY + } + active { + backgroundColor = INACTIVE_GRAY + } + } +} diff --git a/src/jsMain/kotlin/ui/components/options/OptionsPage.kt b/src/jsMain/kotlin/ui/components/options/OptionsPage.kt index 00145ef8..aa083a7e 100644 --- a/src/jsMain/kotlin/ui/components/options/OptionsPage.kt +++ b/src/jsMain/kotlin/ui/components/options/OptionsPage.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.* import kotlinx.css.* import kotlinx.css.properties.border import mainScope +import model.BundleValidationRule import model.CliContext import model.PackageInfo import react.* @@ -26,13 +27,15 @@ private const val TERMINOLOGY_CHECK_TIME_LIMIT = 20000L external interface OptionsPageProps : Props { var cliContext: CliContext - var selectedIgPackageInfo: Set + var igPackageInfoSet: Set var updateCliContext: (CliContext) -> Unit - var updateSelectedIgPackageInfo: (Set) -> Unit - var addedExtensionInfo: Set - var updateAddedExtensionUrl: (Set) -> Unit - var addedProfiles: Set - var updateAddedProfiles: (Set) -> Unit + var updateIgPackageInfoSet: (Set) -> Unit + var extensionSet: Set + var updateExtensionSet: (Set) -> Unit + var profileSet: Set + var updateProfileSet: (Set) -> Unit + var bundleValidationRuleSet : Set + var updateBundleValidationRuleSet : (Set) -> Unit var polyglot: Polyglot var setSessionId: (String) -> Unit } @@ -67,15 +70,14 @@ class OptionsPage : RComponent() { private fun updateCliContext(cliContext: CliContext) { props.updateCliContext(cliContext) props.setSessionId(""); - console.log("Ungabunga") } - private fun updateAddedExtensions(newAddedExtensionSet: MutableSet) { - props.updateAddedExtensionUrl(newAddedExtensionSet) + private fun updateExtensionSet(newExtensionSet: MutableSet) { + props.updateExtensionSet(newExtensionSet) props.setSessionId(""); - console.log("Ungabunga 2") } + override fun RBuilder.render() { styledDiv { css { @@ -169,6 +171,21 @@ class OptionsPage : RComponent() { updateCliContext(props.cliContext) } } + styledDiv { + css { + +OptionsPageStyle.optionsDivider + } + } + checkboxWithDetails { + name = props.polyglot.t("options_flags_check_ips_codes_title") + description = props.polyglot.t("options_flags_check_ips_codes_description") + selected = props.cliContext.isCheckIPSCodes() + hasDescription = true + onChange = { + props.cliContext.setCheckIPSCodes(it) + updateCliContext(props.cliContext) + } + } } heading { text = props.polyglot.t("options_fhir_title") @@ -214,14 +231,14 @@ class OptionsPage : RComponent() { igPackageNameList = state.igPackageNameList.map{ Pair(it.first, it.first == igPackageName)}.toMutableList() } } - selectedIgSet = props.selectedIgPackageInfo.toMutableSet() + selectedIgSet = props.igPackageInfoSet.toMutableSet() onUpdateIg = { igPackageInfo, selected -> val newSelectedIgSet = if (selected) { selectedIgSet.plus(igPackageInfo).toMutableSet() } else { selectedIgSet.minus(igPackageInfo).toMutableSet() } - props.updateSelectedIgPackageInfo(newSelectedIgSet) + props.updateIgPackageInfoSet(newSelectedIgSet) props.setSessionId("") } onFilterStringChange = { partialIgName -> @@ -244,16 +261,17 @@ class OptionsPage : RComponent() { } addProfile { polyglot = props.polyglot - addedProfiles = props.addedProfiles.toMutableSet() + profileSet = props.profileSet.toMutableSet() updateCliContext = updateCliContext cliContext = cliContext - onUpdateProfiles = { profile , delete -> - val newProfiles = if (delete) { - addedProfiles.minus(profile).toMutableSet() + onUpdateProfileSet = { profile, delete -> + val newProfileSet = if (delete) { + profileSet.minus(profile).toMutableSet() } else { - addedProfiles.plus(profile).toMutableSet() + profileSet.plus(profile).toMutableSet() } - props.updateAddedProfiles(newProfiles) + props.updateProfileSet(newProfileSet) + props.setSessionId("") } } } @@ -266,24 +284,48 @@ class OptionsPage : RComponent() { } addExtension { polyglot = props.polyglot - addedExtensionSet = props.addedExtensionInfo.toMutableSet() + extensionSet = props.extensionSet.toMutableSet() updateCliContext = updateCliContext cliContext = cliContext - onUpdateExtension = { extensionUrl , delete -> - val newAddedExtensionSet = if (delete) { - addedExtensionSet.minus(extensionUrl).toMutableSet() + onUpdateExtensionSet = { extensionUrl, delete -> + val newExtensionSet = if (delete) { + extensionSet.minus(extensionUrl).toMutableSet() } else { - addedExtensionSet.plus(extensionUrl).toMutableSet() + extensionSet.plus(extensionUrl).toMutableSet() } - updateAddedExtensions(newAddedExtensionSet) + updateExtensionSet(newExtensionSet) } onUpdateAnyExtension = {anySelected -> - val newSet = if (anySelected) { - addedExtensionSet.plus("any").toMutableSet() + val newExtensionSet = if (anySelected) { + extensionSet.plus("any").toMutableSet() + } else { + extensionSet.minus("any").toMutableSet() + } + updateExtensionSet(newExtensionSet) + } + } + } + heading { + text = props.polyglot.t("options_bundle_validation_rules_title") + } + styledDiv { + css { + +OptionsPageStyle.optionsSubSection + } + addBundleValidationRule { + polyglot = props.polyglot + bundleValidationRuleSet = props.bundleValidationRuleSet.toMutableSet() + updateCliContext = updateCliContext + cliContext = cliContext + onUpdateBundleValidationRuleSet = { rule, delete -> + val bundleValidationRuleSet = props.bundleValidationRuleSet.toMutableSet() + val newBundleValidationRuleSet = if (delete) { + bundleValidationRuleSet.minus(rule).toMutableSet() } else { - addedExtensionSet.minus("any").toMutableSet() + bundleValidationRuleSet.plus(rule).toMutableSet() } - updateAddedExtensions(newSet) + props.updateBundleValidationRuleSet(newBundleValidationRuleSet) + props.setSessionId("") } } } diff --git a/src/jsMain/kotlin/ui/components/options/PresetSelect.kt b/src/jsMain/kotlin/ui/components/options/PresetSelect.kt new file mode 100644 index 00000000..e6934c35 --- /dev/null +++ b/src/jsMain/kotlin/ui/components/options/PresetSelect.kt @@ -0,0 +1,139 @@ +package ui.components.options + +import Polyglot +import mui.material.* +import react.* +import csstype.px +import kotlinx.coroutines.launch +import kotlinx.css.* +import model.CliContext +import mainScope +import model.BundleValidationRule +import model.PackageInfo +import mui.system.sx +import popper.core.Placement +import react.Props +import react.ReactNode + +import styled.css +import styled.styledDiv + +import utils.Preset +import utils.getJS + + +external interface PresetSelectProps : Props { + var cliContext: CliContext + var updateCliContext: (CliContext) -> Unit + var updateIgPackageInfoSet: (Set) -> Unit + var updateExtensionSet: (Set) -> Unit + var updateProfileSet: (Set) -> Unit + var updateBundleValidationRuleSet: (Set) -> Unit + var setSessionId: (String) -> Unit + var polyglot: Polyglot +} + +class PresetSelectState : State { + var snackbarOpen : String? = null +} + +class PresetSelect : RComponent() { + + init { + state = PresetSelectState() + } + + fun handleSnackBarClose() { + mainScope.launch { + setState { + snackbarOpen = null + } + } + } + override fun RBuilder.render() { + styledDiv { + css { + display = Display.inlineFlex + flexDirection = FlexDirection.column + alignSelf = Align.center + + } + Tooltip { + attrs { + title = ReactNode( + props.polyglot.t("preset_description") + ) + placement = TooltipPlacement.left + } + Box { + attrs { + sx { + minWidth = 270.px + } + } + FormControl { + attrs { + fullWidth = true + size = Size.medium + } + InputLabel { + +props.polyglot.t("preset_label") + } + Select { + attrs { + label = ReactNode("preset") + onChange = { event, _ -> + val selectedPreset = Preset.getSelectedPreset(event.target.value) + if (selectedPreset != null) { + console.log("updating cli context for preset: " + event.target.value) + props.updateCliContext(selectedPreset.cliContext) + props.updateIgPackageInfoSet(selectedPreset.igPackageInfo) + props.updateExtensionSet(selectedPreset.extensionSet) + props.updateProfileSet(selectedPreset.profileSet) + props.updateBundleValidationRuleSet( + selectedPreset.cliContext.getBundleValidationRules().toMutableSet() + ) + mainScope.launch { + setState { + snackbarOpen = selectedPreset.polyglotKey + } + } + props.setSessionId("") + } + } + + } + + Preset.values().forEach { + MenuItem { + attrs { + value = it.key + } + +props.polyglot.t(it.polyglotKey) + } + } + } + } + } + } + Snackbar { + attrs { + open = state.snackbarOpen != null + message = ReactNode( + props.polyglot.t("preset_notification", + getJS(arrayOf(Pair("selectedPreset", props.polyglot.t(state.snackbarOpen.toString()))))) + ) + autoHideDuration=6000 + onClose = { event, _ -> handleSnackBarClose() } + } + } + } + } +} + +fun RBuilder.presetSelect(handler: PresetSelectProps.() -> Unit) { + return child(PresetSelect::class) { + this.attrs(handler) + } +} + diff --git a/src/jsMain/kotlin/ui/components/options/ExtensionDisplay.kt b/src/jsMain/kotlin/ui/components/options/UrlDisplay.kt similarity index 76% rename from src/jsMain/kotlin/ui/components/options/ExtensionDisplay.kt rename to src/jsMain/kotlin/ui/components/options/UrlDisplay.kt index d5e2cee3..fe4cdd10 100644 --- a/src/jsMain/kotlin/ui/components/options/ExtensionDisplay.kt +++ b/src/jsMain/kotlin/ui/components/options/UrlDisplay.kt @@ -10,28 +10,28 @@ import react.* import react.dom.attrs import styled.* -external interface ExtensionDisplayProps : Props { +external interface UrlDisplayProps : Props { var url: String var polyglot: Polyglot var onDelete: () -> Unit } -class ExtensionDisplay : RComponent() { +class UrlDisplay : RComponent() { override fun RBuilder.render() { styledDiv { css { - +ExtensionDisplayStyle.mainDiv + +UrlDisplayStyle.mainDiv } styledSpan { css { +TextStyle.dropDownLabel - +ExtensionDisplayStyle.extensionName + +UrlDisplayStyle.extensionName } +props.url } styledImg { css { - +ExtensionDisplayStyle.closeButton + +UrlDisplayStyle.closeButton } attrs { src = "images/close_black.png" @@ -47,8 +47,8 @@ class ExtensionDisplay : RComponent() { /** * React Component Builder */ -fun RBuilder.extensionDisplay(handler: ExtensionDisplayProps.() -> Unit) { - return child(ExtensionDisplay::class) { +fun RBuilder.urlDisplay(handler: UrlDisplayProps.() -> Unit) { + return child(UrlDisplay::class) { this.attrs(handler) } } @@ -56,7 +56,7 @@ fun RBuilder.extensionDisplay(handler: ExtensionDisplayProps.() -> Unit) { /** * CSS */ -object ExtensionDisplayStyle : StyleSheet("ExtensionDisplayStyle", isStatic = true) { +object UrlDisplayStyle : StyleSheet("UrlDisplayStyle", isStatic = true) { val mainDiv by css { display = Display.flex flexDirection = FlexDirection.row diff --git a/src/jsMain/kotlin/ui/components/options/menu/TextFieldEntry.kt b/src/jsMain/kotlin/ui/components/options/menu/TextFieldEntry.kt index 3439c5d5..3cd21bd3 100644 --- a/src/jsMain/kotlin/ui/components/options/menu/TextFieldEntry.kt +++ b/src/jsMain/kotlin/ui/components/options/menu/TextFieldEntry.kt @@ -21,7 +21,7 @@ import react.dom.defaultValue import styled.* import ui.components.buttons.imageButton import ui.components.options.IgSelectorStyle -import ui.components.tabs.entrytab.ManualEntryButtonBarStyle +import ui.components.tabs.entrytab.ManualValidateButtonStyle external interface TextFieldEntryProps : Props { var onSubmitEntry: (String) -> Deferred @@ -64,7 +64,7 @@ class TextFieldEntry : RComponent() { } styledDiv { css { - +TextFieldEntryStyle.textFieldAndGButtonDiv + +TextFieldEntryStyle.textFieldAndAddButtonDiv } styledInput { css { @@ -85,7 +85,7 @@ class TextFieldEntry : RComponent() { if (state.validating) { styledDiv { css { - +ManualEntryButtonBarStyle.spinner + +ManualValidateButtonStyle.spinner } } } else { @@ -161,7 +161,7 @@ object TextFieldEntryStyle : StyleSheet("TextFieldEntryStyle", isStatic = true) val detailsText by css { padding(top = 8.px, bottom = 16.px) } - val textFieldAndGButtonDiv by css { + val textFieldAndAddButtonDiv by css { display = Display.flex flexDirection = FlexDirection.row alignItems = Align.center diff --git a/src/jsMain/kotlin/ui/components/tabs/entrytab/ManualEntryTab.kt b/src/jsMain/kotlin/ui/components/tabs/entrytab/ManualEntryTab.kt index ea98c7f6..4b44408e 100644 --- a/src/jsMain/kotlin/ui/components/tabs/entrytab/ManualEntryTab.kt +++ b/src/jsMain/kotlin/ui/components/tabs/entrytab/ManualEntryTab.kt @@ -2,29 +2,35 @@ package ui.components.tabs.entrytab import Polyglot import api.sendValidationRequest -import constants.FhirFormat + import css.animation.FadeIn.fadeIn import css.const.BORDER_GRAY import css.text.TextStyle + import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.css.* import kotlinx.css.properties.border import mainScope +import model.BundleValidationRule import model.CliContext +import model.PackageInfo import model.ValidationOutcome import react.* import react.dom.attrs + import styled.* +import ui.components.options.presetSelect import ui.components.tabs.heading + import ui.components.validation.issuelist.filteredIssueEntryList import utils.assembleRequest import utils.isJson import utils.isXml //TODO make this an intelligent value -private const val VALIDATION_TIME_LIMIT = 45000L +private const val VALIDATION_TIME_LIMIT = 60000L external interface ManualEntryTabProps : Props { var cliContext: CliContext @@ -37,6 +43,11 @@ external interface ManualEntryTabProps : Props { var setValidationOutcome: (ValidationOutcome) -> Unit var toggleValidationInProgress: (Boolean) -> Unit var updateCurrentlyEnteredText: (String) -> Unit + var updateCliContext: (CliContext) -> Unit + var updateIgPackageInfoSet: (Set) -> Unit + var updateExtensionSet: (Set) -> Unit + var updateProfileSet: (Set) -> Unit + var updateBundleValidationRuleSet: (Set) -> Unit var setSessionId: (String) -> Unit } @@ -82,20 +93,40 @@ class ManualEntryTab : RComponent() { } } } - manualEntryButtonBar { - validateText = props.polyglot.t("validate_button") - onValidateRequested = { - if (props.currentManuallyEnteredText.isNotEmpty()) { - validateEnteredText(props.currentManuallyEnteredText) - } else { - val newErrorMessage = props.polyglot.t("manual_entry_error") - setState { - errorMessage = newErrorMessage - displayingError = true + styledDiv { + css { + +ManualEntryTabStyle.buttonBar + } + manualEntryValidateButton { + validateText = props.polyglot.t("validate_button") + onValidateRequested = { + if (props.currentManuallyEnteredText.isNotEmpty()) { + validateEnteredText(props.currentManuallyEnteredText) + } else { + val newErrorMessage = props.polyglot.t("manual_entry_error") + setState { + errorMessage = newErrorMessage + displayingError = true + } } } + workInProgress = props.validatingManualEntryInProgress + } + styledDiv{ + css { + +ManualEntryTabStyle.buttonBarDivider + } + } + presetSelect{ + cliContext = props.cliContext + updateCliContext = props.updateCliContext + updateIgPackageInfoSet = props.updateIgPackageInfoSet + updateExtensionSet = props.updateExtensionSet + updateProfileSet = props.updateProfileSet + updateBundleValidationRuleSet = props.updateBundleValidationRuleSet + setSessionId = props.setSessionId + polyglot = props.polyglot } - workInProgress = props.validatingManualEntryInProgress } if (state.displayingError) { styledSpan { @@ -185,6 +216,14 @@ object ManualEntryTabStyle : StyleSheet("ManualEntryTabStyle") { padding(horizontal = 32.px, vertical = 16.px) fadeIn() } + val buttonBar by css { + display = Display.inlineFlex + flexDirection = FlexDirection.row + alignItems = Align.center + } + val buttonBarDivider by css { + width = 16.px + } val ken by css { display = Display.flex flexDirection = FlexDirection.column diff --git a/src/jsMain/kotlin/ui/components/tabs/entrytab/ManualEntryButtonBar.kt b/src/jsMain/kotlin/ui/components/tabs/entrytab/ManualValidateButton.kt similarity index 86% rename from src/jsMain/kotlin/ui/components/tabs/entrytab/ManualEntryButtonBar.kt rename to src/jsMain/kotlin/ui/components/tabs/entrytab/ManualValidateButton.kt index 69cf35cf..6d73e524 100644 --- a/src/jsMain/kotlin/ui/components/tabs/entrytab/ManualEntryButtonBar.kt +++ b/src/jsMain/kotlin/ui/components/tabs/entrytab/ManualValidateButton.kt @@ -24,12 +24,12 @@ class ManualEntryButtonBar : RComponent() { override fun RBuilder.render() { styledDiv { css { - +ManualEntryButtonBarStyle.buttonBarContainer + +ManualValidateButtonStyle.buttonBarContainer } if (props.workInProgress) { styledDiv { css { - +ManualEntryButtonBarStyle.spinner + +ManualValidateButtonStyle.spinner } } } else { @@ -50,7 +50,7 @@ class ManualEntryButtonBar : RComponent() { /** * React Component Builder */ -fun RBuilder.manualEntryButtonBar(handler: ManualEntryButtonBarProps.() -> Unit) { +fun RBuilder.manualEntryValidateButton(handler: ManualEntryButtonBarProps.() -> Unit) { return child(ManualEntryButtonBar::class) { this.attrs(handler) } @@ -59,7 +59,7 @@ fun RBuilder.manualEntryButtonBar(handler: ManualEntryButtonBarProps.() -> Unit) /** * CSS */ -object ManualEntryButtonBarStyle : StyleSheet("ManualEntryButtonBarStyle", isStatic = true) { +object ManualValidateButtonStyle : StyleSheet("ManualEntryButtonBarStyle", isStatic = true) { val buttonBarContainer by css { display = Display.inlineFlex flexDirection = FlexDirection.row diff --git a/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadButton.kt b/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadButton.kt new file mode 100644 index 00000000..f6cbc665 --- /dev/null +++ b/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadButton.kt @@ -0,0 +1,53 @@ +package ui.components.tabs.uploadtab + +import Polyglot +import css.const.HL7_RED +import css.const.WHITE + +import react.* +import styled.StyleSheet + +import ui.components.buttons.imageButton + + +external interface FileUploadButtonProps : Props { + var onUploadRequested: () -> Unit + var onValidateRequested: () -> Unit + + var polyglot: Polyglot + +} + +/** + * Component displaying the horizontal list of buttons for file upload and validation + */ +class FileUploadButton : RComponent() { + + override fun RBuilder.render() { + imageButton { + backgroundColor = WHITE + borderColor = HL7_RED + image = "images/upload_red.png" + label = props.polyglot.t("upload_button") + onSelected = { + props.onUploadRequested() + } + } + } +} + +/** + * React Component Builder + */ +fun RBuilder.fileUploadButton(handler: FileUploadButtonProps.() -> Unit) { + return child(FileUploadButton::class) { + this.attrs(handler) + } +} + +/** + * CSS + */ +object FileUploadButtonStyle : StyleSheet("FileUploadButtonStyle", isStatic = true) { + +} \ No newline at end of file diff --git a/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadButtonBar.kt b/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadButtonBar.kt deleted file mode 100644 index 81810620..00000000 --- a/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadButtonBar.kt +++ /dev/null @@ -1,101 +0,0 @@ -package ui.components.tabs.uploadtab - -import Polyglot -import css.animation.LoadingSpinner -import css.const.HL7_RED -import css.const.WHITE -import kotlinx.css.* -import react.* -import styled.StyleSheet -import styled.css -import styled.styledDiv -import ui.components.buttons.imageButton -import ui.components.tabs.entrytab.ManualEntryButtonBarStyle - -external interface FileUploadButtonBarProps : Props { - var onUploadRequested: () -> Unit - var onValidateRequested: () -> Unit - var workInProgress: Boolean - var polyglot: Polyglot - var uploadText: String - var validateText: String -} - -/** - * Component displaying the horizontal list of buttons for file upload and validation - */ -class FileUploadButtonBar : RComponent() { - - override fun RBuilder.render() { - styledDiv { - css { - +FileUploadButtonBarStyle.buttonBarContainer - } - - imageButton { - backgroundColor = WHITE - borderColor = HL7_RED - image = "images/upload_red.png" - label = props.polyglot.t("upload_button") - onSelected = { - props.onUploadRequested() - } - } - - styledDiv { - css { - +FileUploadButtonBarStyle.buttonBarDivider - } - } - - if (props.workInProgress) { - styledDiv { - css { - +ManualEntryButtonBarStyle.spinner - } - } - } else { - imageButton { - backgroundColor = WHITE - borderColor = HL7_RED - image = "images/validate_red.png" - label = props.polyglot.t("validate_button") - onSelected = { - props.onValidateRequested() - } - } - } - } - } -} - -/** - * React Component Builder - */ -fun RBuilder.fileUploadButtonBar(handler: FileUploadButtonBarProps.() -> Unit) { - return child(FileUploadButtonBar::class) { - this.attrs(handler) - } -} - -/** - * CSS - */ -object FileUploadButtonBarStyle : StyleSheet("FileUploadButtonBarStyle", isStatic = true) { - val buttonBarContainer by css { - display = Display.inlineFlex - flexDirection = FlexDirection.row - alignItems = Align.center - padding(vertical = 16.px) - } - val buttonBarDivider by css { - width = 16.px - } - val spinner by css { - height = 32.px - width = 32.px - margin(horizontal = 32.px, vertical = 8.px) - alignSelf = Align.center - +LoadingSpinner.loadingIndicator - } -} \ No newline at end of file diff --git a/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadTab.kt b/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadTab.kt index b54b1a70..8b14768d 100644 --- a/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadTab.kt +++ b/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileUploadTab.kt @@ -8,14 +8,13 @@ import kotlinx.browser.document import kotlinx.coroutines.launch import kotlinx.css.* import mainScope -import model.CliContext -import model.FileInfo -import model.ValidationOutcome +import model.* import org.w3c.dom.HTMLInputElement import react.* import styled.StyleSheet import styled.css import styled.styledDiv +import ui.components.options.presetSelect import ui.components.tabs.heading import ui.components.tabs.uploadtab.filelist.fileEntryList import ui.components.validation.validationSummaryPopup @@ -29,9 +28,15 @@ external interface FileUploadTabProps : Props { var deleteFile: (FileInfo) -> Unit var uploadFile: (FileInfo) -> Unit - var setSessionId: (String) -> Unit var toggleValidationInProgress: (Boolean, FileInfo) -> Unit var addValidationOutcome: (ValidationOutcome) -> Unit + + var updateCliContext: (CliContext) -> Unit + var updateIgPackageInfoSet: (Set) -> Unit + var updateExtensionSet: (Set) -> Unit + var updateProfileSet: (Set) -> Unit + var updateBundleValidationRuleSet: (Set) -> Unit + var setSessionId: (String) -> Unit } class FileUploadTabState : State { @@ -50,7 +55,7 @@ class FileUploadTab : RComponent() { override fun RBuilder.render() { styledDiv { css { - +TabStyle.tabContent + +FileUploadTabStyle.tabContent } heading { text = props.polyglot.t("upload_files_title") + "(${props.uploadedFiles.size})" @@ -67,15 +72,48 @@ class FileUploadTab : RComponent() { props.deleteFile(it.getFileInfo()) } } - fileUploadButtonBar { - polyglot = props.polyglot - onUploadRequested = { - (document.getElementById(FILE_UPLOAD_ELEMENT_ID) as HTMLInputElement).click() + styledDiv { + css { + +FileUploadTabStyle.buttonBarContainer + } + fileUploadButton { + polyglot = props.polyglot + onUploadRequested = { + (document.getElementById(FILE_UPLOAD_ELEMENT_ID) as HTMLInputElement).click() + } + } + + styledDiv { + css { + +FileUploadTabStyle.buttonBarDivider + } + } + + fileValidateButton { + polyglot = props.polyglot + onValidateRequested = { + validateUploadedFiles() + } } - onValidateRequested = { - validateUploadedFiles() + + styledDiv{ + css { + +FileUploadTabStyle.buttonBarDivider + } + } + + presetSelect{ + cliContext = props.cliContext + updateCliContext = props.updateCliContext + updateIgPackageInfoSet = props.updateIgPackageInfoSet + updateExtensionSet = props.updateExtensionSet + updateProfileSet = props.updateProfileSet + updateBundleValidationRuleSet = props.updateBundleValidationRuleSet + setSessionId = props.setSessionId + polyglot = props.polyglot } } + uploadFilesComponent { onFileUpload = { props.uploadFile(it) @@ -131,15 +169,22 @@ fun RBuilder.fileUploadTab(handler: FileUploadTabProps.() -> Unit) { /** * CSS */ -object TabStyle : StyleSheet("TabStyle", isStatic = true) { +object FileUploadTabStyle : StyleSheet("FileUploadTabStyle", isStatic = true) { val tabContent by css { backgroundColor = WHITE flexDirection = FlexDirection.column justifyContent = JustifyContent.flexStart - alignItems = Align.flexStart display = Display.flex padding(horizontal = 32.px, vertical = 16.px) fadeIn() - flex(flexBasis = 100.pct) + } + val buttonBarContainer by css { + display = Display.inlineFlex + flexDirection = FlexDirection.row + alignItems = Align.center + padding(vertical = 16.px) + } + val buttonBarDivider by css { + width = 16.px } } \ No newline at end of file diff --git a/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileValidateButton.kt b/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileValidateButton.kt new file mode 100644 index 00000000..a7cffdde --- /dev/null +++ b/src/jsMain/kotlin/ui/components/tabs/uploadtab/FileValidateButton.kt @@ -0,0 +1,68 @@ +package ui.components.tabs.uploadtab + +import Polyglot +import css.animation.LoadingSpinner +import css.const.HL7_RED +import css.const.WHITE +import kotlinx.css.* +import react.* +import styled.StyleSheet +import styled.css +import styled.styledDiv +import ui.components.buttons.imageButton + +external interface FileValidateButtonProps : Props { + var onValidateRequested: () -> Unit + var workInProgress: Boolean + var polyglot: Polyglot +} + +/** + * Component displaying the horizontal list of buttons for file upload and validation + */ +class FileValidateButton : RComponent() { + + override fun RBuilder.render() { + if (props.workInProgress) { + styledDiv { + css { + +FileValidateButtonStyle.spinner + } + } + } else { + imageButton { + backgroundColor = WHITE + borderColor = HL7_RED + image = "images/validate_red.png" + label = props.polyglot.t("validate_button") + onSelected = { + props.onValidateRequested() + } + } + } + } + +} + +/** + * React Component Builder + */ +fun RBuilder.fileValidateButton(handler: FileValidateButtonProps.() -> Unit) { + return child(FileValidateButton::class) { + this.attrs(handler) + } +} + +/** + * CSS + */ +object FileValidateButtonStyle : StyleSheet("FileValidateButtonStyle", isStatic = true) { + + val spinner by css { + height = 32.px + width = 32.px + margin(horizontal = 32.px, vertical = 8.px) + alignSelf = Align.center + +LoadingSpinner.loadingIndicator + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/utils/Language.kt b/src/jsMain/kotlin/utils/Language.kt index ccafa588..67125914 100644 --- a/src/jsMain/kotlin/utils/Language.kt +++ b/src/jsMain/kotlin/utils/Language.kt @@ -8,12 +8,16 @@ enum class Language(val code: String, val display: String) { fun getLanguageCode() : String { return this.code.substring(0, 2) } -} -fun getSelectedLanguage(targetLanguage: String) : Language? { - for (language in Language.values()) { - if (targetLanguage == language.getLanguageCode()) { - return language + companion object { + + fun getSelectedLanguage(targetLanguage: String) : Language? { + for (language in Language.values()) { + if (targetLanguage == language.getLanguageCode()) { + return language + } } + return null + } } - return null } + diff --git a/src/jsMain/kotlin/utils/Preset.kt b/src/jsMain/kotlin/utils/Preset.kt new file mode 100644 index 00000000..9fbf6b18 --- /dev/null +++ b/src/jsMain/kotlin/utils/Preset.kt @@ -0,0 +1,142 @@ +package utils + +import model.CliContext +import model.PackageInfo + +import constants.ANY_EXTENSION +import model.BundleValidationRule + +val DEFAULT_CONTEXT = CliContext() + +val IPS_IG = PackageInfo( + "hl7.fhir.uv.ips", + "1.1.0", + "4.0.1", + "http://hl7.org/fhir/uv/ips/STU1.1" +) + +val IPS_AU_IG = PackageInfo( + "hl7.fhir.au.ips", + "current", + "4.0.1", + "http://hl7.org.au/fhir/ips/ImplementationGuide/hl7.fhir.au.ips" +) + +val CDA_IG = PackageInfo( + "hl7.cda.uv.core", + "2.1.0-draft1", + "5.0.0", + "http://hl7.org/cda/stds/core/ImplementationGuide/hl7.cda.uv.core" +) + +val SQL_ON_FHIR_IG = PackageInfo( + "hl7.fhir.uv.sql-on-fhir", + "current", + "5.0.0", + "http://hl7.org/fhir/uv/sql-on-fhir/ImplementationGuide/hl7.fhir.uv.sql-on-fhir" +) + +val IPS_BUNDLE_PROFILE = "http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips" + +val IPS_AU_BUNDLE_PROFILE = "http://hl7.org.au/fhir/ips/StructureDefinition/Bundle-au-ips" + +val IPS_CONTEXT = CliContext() + .setSv("4.0.1") + .addIg(PackageInfo.igLookupString(IPS_IG)) + .setExtensions(listOf(ANY_EXTENSION)) + .setCheckIPSCodes(true) + .setBundleValidationRules(listOf( + BundleValidationRule() + .setRule("Composition:0") + .setProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/Composition-uv-ips") + )) + +val IPS_AU_CONTEXT = CliContext() + .setSv("4.0.1") + .addIg(PackageInfo.igLookupString(IPS_AU_IG)) + .setExtensions(listOf(ANY_EXTENSION)) + .setCheckIPSCodes(true) + .setBundleValidationRules(listOf( + BundleValidationRule() + .setRule("Composition:0") + .setProfile("http://hl7.org.au/fhir/ips/StructureDefinition/Composition-au-ips") + )) + +val CDA_CONTEXT = CliContext() + .setSv("5.0.0") + .addIg(PackageInfo.igLookupString(CDA_IG)) + +val SQL_VIEW_CONTEXT = CliContext() + .setSv("5.0.0") + .addIg(PackageInfo.igLookupString(SQL_ON_FHIR_IG)) + +enum class Preset( + val key: String, + val polyglotKey: String, + val cliContext: CliContext, + val igPackageInfo: Set, + val extensionSet: Set, + val profileSet: Set +) { + DEFAULT( + "DEFAULT", + "preset_fhir_resource", + DEFAULT_CONTEXT, + setOf(), + setOf(), + setOf() + ), + IPS_CURRENT( + "IPS", + "preset_ips", + IPS_CONTEXT, + setOf(IPS_IG), + setOf(ANY_EXTENSION), + setOf(IPS_BUNDLE_PROFILE) + ), + IPS_AU( + "IPS_AU", + "preset_ips_au", + IPS_AU_CONTEXT, + setOf(IPS_AU_IG), + setOf(ANY_EXTENSION), + setOf(IPS_AU_BUNDLE_PROFILE) + ), + CDA( + "CDA", + "present_cda", + CDA_CONTEXT, + setOf(CDA_IG), + setOf(), + setOf() + ), + US_CCDA( + "US_CDA", + "preset_us_ccda", + CDA_CONTEXT, + setOf(CDA_IG), + setOf(), + setOf() + ), + SQL_VIEW( + "SQL_VIEW", + "preset_sql_view", + SQL_VIEW_CONTEXT, + setOf(SQL_ON_FHIR_IG), + setOf(), + setOf() + ) + ; + + companion object { + fun getSelectedPreset(key: String?): Preset? { + for (preset in Preset.values()) { + if (key == preset.key) { + return preset + } + } + return null + } + } +} + diff --git a/src/jvmMain/kotlin/model/BundleValidationRule.kt b/src/jvmMain/kotlin/model/BundleValidationRule.kt new file mode 100644 index 00000000..8c97e3c5 --- /dev/null +++ b/src/jvmMain/kotlin/model/BundleValidationRule.kt @@ -0,0 +1,3 @@ +package model + +actual typealias BundleValidationRule = org.hl7.fhir.r5.utils.validation.BundleValidationRule