From 3761c5051df511f85f1f532e0447327dcb5a7741 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Wed, 21 Jul 2021 14:06:22 -0400 Subject: [PATCH 1/2] Add a new path properties presentation option for same-path visits with query strings --- .../main/assets/json/test-configuration.json | 9 +++ .../turbo/config/TurboPathConfiguration.kt | 9 +++ .../nav/TurboNavQueryStringPresentation.kt | 27 +++++++++ .../dev/hotwire/turbo/nav/TurboNavRule.kt | 19 ++++-- .../dev/hotwire/turbo/nav/TurboNavRuleTest.kt | 59 ++++++++++++++++++- 5 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavQueryStringPresentation.kt diff --git a/turbo/src/main/assets/json/test-configuration.json b/turbo/src/main/assets/json/test-configuration.json index 4b86aeec..bb1591a6 100644 --- a/turbo/src/main/assets/json/test-configuration.json +++ b/turbo/src/main/assets/json/test-configuration.json @@ -23,6 +23,14 @@ "presentation": "clear_all" } }, + { + "patterns": [ + "/feature" + ], + "properties": { + "query_string_presentation": "replace" + } + }, { "patterns": [ "/new$", @@ -31,6 +39,7 @@ "properties": { "context": "modal", "uri": "turbo://fragment/web/modal", + "query_string_presentation": "default", "pull_to_refresh_enabled": false } }, diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/config/TurboPathConfiguration.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/config/TurboPathConfiguration.kt index b7335662..6fe42478 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/config/TurboPathConfiguration.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/config/TurboPathConfiguration.kt @@ -6,6 +6,7 @@ import android.net.Uri import dev.hotwire.turbo.nav.TurboNavPresentation import dev.hotwire.turbo.nav.TurboNavPresentationContext import com.google.gson.annotations.SerializedName +import dev.hotwire.turbo.nav.TurboNavQueryStringPresentation import java.net.URL /** @@ -110,6 +111,14 @@ val TurboPathConfigurationProperties.presentation: TurboNavPresentation TurboNavPresentation.DEFAULT } +val TurboPathConfigurationProperties.queryStringPresentation: TurboNavQueryStringPresentation + @SuppressLint("DefaultLocale") get() = try { + val value = get("query_string_presentation") ?: "default" + TurboNavQueryStringPresentation.valueOf(value.toUpperCase()) + } catch (e: IllegalArgumentException) { + TurboNavQueryStringPresentation.DEFAULT + } + val TurboPathConfigurationProperties.context: TurboNavPresentationContext @SuppressLint("DefaultLocale") get() = try { val value = get("context") ?: "default" diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavQueryStringPresentation.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavQueryStringPresentation.kt new file mode 100644 index 00000000..7583cf3b --- /dev/null +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavQueryStringPresentation.kt @@ -0,0 +1,27 @@ +package dev.hotwire.turbo.nav + +/** + * Represents how a given navigation destination should be presented when the current + * location path on the backstack matches the new location path *and* a query string is + * present in either location. + * + * Example situation: + * current location: /feature + * new location: /feature?filter=true + */ +enum class TurboNavQueryStringPresentation { + /** + * A generic default value when no specific presentation value is provided and results in + * generally accepted "normal" behavior — replacing the root when on the start destination and + * going to the start destination again, popping when the location is in the immediate + * backstack, replacing when going to the same destination, and pushing in all other cases. + */ + DEFAULT, + + /** + * Pops the current location off the nav stack and pushes the new location onto the nav stack. + * If you use query strings in your app to act as a way to filter results in a destination, + * this allows you to present the new (filtered) destination without adding onto the backstack. + */ + REPLACE +} diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt index 34d779a4..48e07e81 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt @@ -36,6 +36,7 @@ internal class TurboNavRule( val newExtras = extras val newProperties = pathConfiguration.properties(newLocation) val newPresentationContext = newProperties.context + val newQueryStringPresentation = newProperties.queryStringPresentation val newPresentation = newPresentation() val newNavigationMode = newNavigationMode() val newModalResult = newModalResult() @@ -55,8 +56,8 @@ internal class TurboNavRule( return newProperties.presentation } - val locationIsCurrent = locationPathsAreEqual(newLocation, currentLocation) - val locationIsPrevious = locationPathsAreEqual(newLocation, previousLocation) + val locationIsCurrent = locationsAreSame(newLocation, currentLocation) + val locationIsPrevious = locationsAreSame(newLocation, previousLocation) val replace = newVisitOptions.action == TurboVisitAction.REPLACE return when { @@ -132,11 +133,21 @@ internal class TurboNavRule( private val NavBackStackEntry?.location: String? get() = this?.arguments?.getString("location") - private fun locationPathsAreEqual(first: String?, second: String?): Boolean { + private fun locationsAreSame(first: String?, second: String?): Boolean { if (first == null || second == null) { return false } - return Uri.parse(first).path == Uri.parse(second).path + val firstUri = Uri.parse(first) + val secondUri = Uri.parse(second) + + return when (newQueryStringPresentation) { + TurboNavQueryStringPresentation.REPLACE -> { + firstUri.path == secondUri.path + } + TurboNavQueryStringPresentation.DEFAULT -> { + firstUri.path == secondUri.path && firstUri.query == secondUri.query + } + } } } diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/nav/TurboNavRuleTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/nav/TurboNavRuleTest.kt index a8c46660..e3883100 100644 --- a/turbo/src/test/kotlin/dev/hotwire/turbo/nav/TurboNavRuleTest.kt +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/nav/TurboNavRuleTest.kt @@ -38,6 +38,8 @@ class TurboNavRuleTest { private val resumeUrl = "https://hotwired.dev/custom/resume" private val modalRootUrl = "https://hotwired.dev/custom/modal" private val filterUrl = "https://hotwired.dev/feature?filter=true" + private val customUrl = "https://hotwired.dev/custom" + private val customQueryUrl = "https://hotwired.dev/custom?id=1" private val webDestinationId = 1 private val webModalDestinationId = 2 @@ -80,6 +82,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(featureUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.PUSH) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.REPLACE) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT) assertThat(rule.newModalResult).isNull() assertThat(rule.newDestinationUri).isEqualTo(webUri) @@ -101,6 +104,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(newUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.MODAL) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.PUSH) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.TO_MODAL) assertThat(rule.newModalResult).isNull() assertThat(rule.newDestinationUri).isEqualTo(webModalUri) @@ -130,6 +134,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(homeUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.CLEAR_ALL) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT) assertThat(rule.newModalResult).isNull() assertThat(rule.newDestinationUri).isEqualTo(webHomeUri) @@ -153,6 +158,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(featureUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.POP) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.REPLACE) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.DISMISS_MODAL) assertThat(rule.newModalResult?.location).isEqualTo(featureUrl) assertThat(rule.newDestinationUri).isEqualTo(webUri) @@ -175,6 +181,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(newUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.MODAL) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.REPLACE) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT) assertThat(rule.newModalResult).isNull() assertThat(rule.newDestinationUri).isEqualTo(webModalUri) @@ -197,6 +204,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(editUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.MODAL) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.PUSH) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT) assertThat(rule.newModalResult).isNull() assertThat(rule.newDestinationUri).isEqualTo(webModalUri) @@ -219,6 +227,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(refreshUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.REFRESH) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.REFRESH) assertThat(rule.newModalResult).isNull() assertThat(rule.newDestinationUri).isEqualTo(webUri) @@ -242,6 +251,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(resumeUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.NONE) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.DISMISS_MODAL) assertThat(rule.newModalResult).isNotNull() assertThat(rule.newModalResult?.location).isEqualTo(resumeUrl) @@ -252,7 +262,53 @@ class TurboNavRuleTest { } @Test - fun `navigate to the same path with query params`() { + fun `navigate to the same path with new query string`() { + controller.navigate(webDestinationId, locationArgs(customUrl)) + val rule = getNavigatorRule(customQueryUrl) + + // Current destination + assertThat(rule.previousLocation).isEqualTo(homeUrl) + assertThat(rule.currentLocation).isEqualTo(customUrl) + assertThat(rule.currentPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) + assertThat(rule.isAtStartDestination).isFalse() + + // New destination + assertThat(rule.newLocation).isEqualTo(customQueryUrl) + assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) + assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.PUSH) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT) + assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT) + assertThat(rule.newModalResult).isNull() + assertThat(rule.newDestinationUri).isEqualTo(webUri) + assertThat(rule.newDestination).isNotNull() + assertThat(rule.newNavOptions).isEqualTo(navOptions) + } + + @Test + fun `navigate to the same path with same query string`() { + controller.navigate(webDestinationId, locationArgs(customQueryUrl)) + val rule = getNavigatorRule(customQueryUrl) + + // Current destination + assertThat(rule.previousLocation).isEqualTo(homeUrl) + assertThat(rule.currentLocation).isEqualTo(customQueryUrl) + assertThat(rule.currentPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) + assertThat(rule.isAtStartDestination).isFalse() + + // New destination + assertThat(rule.newLocation).isEqualTo(customQueryUrl) + assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) + assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.REPLACE) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.DEFAULT) + assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT) + assertThat(rule.newModalResult).isNull() + assertThat(rule.newDestinationUri).isEqualTo(webUri) + assertThat(rule.newDestination).isNotNull() + assertThat(rule.newNavOptions).isEqualTo(navOptions) + } + + @Test + fun `navigate to the same path with filterable query string`() { controller.navigate(webDestinationId, locationArgs(featureUrl)) val rule = getNavigatorRule(filterUrl) @@ -266,6 +322,7 @@ class TurboNavRuleTest { assertThat(rule.newLocation).isEqualTo(filterUrl) assertThat(rule.newPresentationContext).isEqualTo(TurboNavPresentationContext.DEFAULT) assertThat(rule.newPresentation).isEqualTo(TurboNavPresentation.REPLACE) + assertThat(rule.newQueryStringPresentation).isEqualTo(TurboNavQueryStringPresentation.REPLACE) assertThat(rule.newNavigationMode).isEqualTo(TurboNavMode.IN_CONTEXT) assertThat(rule.newModalResult).isNull() assertThat(rule.newDestinationUri).isEqualTo(webUri) From a5cf43e3789c322946a4eb2ad06be8ac44031c33 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Wed, 21 Jul 2021 14:19:42 -0400 Subject: [PATCH 2/2] Fix tests, since the configuration file was updated --- .../turbo/config/TurboPathConfigurationRepositoryTest.kt | 2 +- .../dev/hotwire/turbo/config/TurboPathConfigurationTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationRepositoryTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationRepositoryTest.kt index 37c3b417..db45b009 100644 --- a/turbo/src/test/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationRepositoryTest.kt +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationRepositoryTest.kt @@ -51,7 +51,7 @@ class TurboPathConfigurationRepositoryTest : BaseRepositoryTest() { assertThat(json).isNotNull() val config = load(json) - assertThat(config?.rules?.size).isEqualTo(7) + assertThat(config?.rules?.size).isEqualTo(8) } @Test diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationTest.kt index c64bd8b4..6a68e609 100644 --- a/turbo/src/test/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationTest.kt +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationTest.kt @@ -38,7 +38,7 @@ class TurboPathConfigurationTest : BaseRepositoryTest() { @Test fun assetConfigurationIsLoaded() { - assertThat(pathConfiguration.rules.size).isEqualTo(7) + assertThat(pathConfiguration.rules.size).isEqualTo(8) } @Test