support 'scope_id' for set_variable actions

commit_hash:86fd118ac6ef2eed66a544a5c95f4e367e548f44
This commit is contained in:
gulevsky
2024-09-20 17:07:59 +03:00
parent 47326aed2b
commit 3e2e12d898
22 changed files with 668 additions and 21 deletions
+5
View File
@@ -1111,6 +1111,7 @@
"client/android/div/src/main/java/com/yandex/div/core/view2/ShadowCache.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/ShadowCache.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/SightActionIsEnabledObserver.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/SightActionIsEnabledObserver.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/ViewBindingProvider.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/ViewBindingProvider.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/ViewLocator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/ViewLocator.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/ViewVisibilityCalculator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/ViewVisibilityCalculator.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/animations/DivComparator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/animations/DivComparator.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/animations/DivComparatorReporter.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/animations/DivComparatorReporter.kt",
@@ -1460,6 +1461,7 @@
"client/android/divkit-demo-app/lint-baseline.xml":"divkit/public/client/android/divkit-demo-app/lint-baseline.xml",
"client/android/divkit-demo-app/proguard-rules.pro":"divkit/public/client/android/divkit-demo-app/proguard-rules.pro",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/AtStartHistogramsTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/AtStartHistogramsTest.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivActionTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivActionTest.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivCollectionAdapterTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivCollectionAdapterTest.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivCustomTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivCustomTest.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivFocusableInputTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivFocusableInputTest.kt",
@@ -18072,6 +18074,9 @@
"test_data/tutorials/counter.json":"divkit/public/test_data/tutorials/counter.json",
"test_data/tutorials/stateful_card.json":"divkit/public/test_data/tutorials/stateful_card.json",
"test_data/tutorials/ticking_timer.json":"divkit/public/test_data/tutorials/ticking_timer.json",
"test_data/ui_test_data/actions/scoped_actions.json":"divkit/public/test_data/ui_test_data/actions/scoped_actions.json",
"test_data/ui_test_data/actions/scoped_actions_ambiguous_scope.json":"divkit/public/test_data/ui_test_data/actions/scoped_actions_ambiguous_scope.json",
"test_data/ui_test_data/actions/scoped_actions_nested_scope.json":"divkit/public/test_data/ui_test_data/actions/scoped_actions_nested_scope.json",
"test_data/ui_test_data/custom/div_custom_several_states_changing.json":"divkit/public/test_data/ui_test_data/custom/div_custom_several_states_changing.json",
"test_data/ui_test_data/custom/div_custom_state_changing.json":"divkit/public/test_data/ui_test_data/custom/div_custom_state_changing.json",
"test_data/ui_test_data/focus/actions.json":"divkit/public/test_data/ui_test_data/focus/actions.json",
@@ -1,17 +1,20 @@
package com.yandex.div.core;
import android.net.Uri;
import android.view.View;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.yandex.div.core.actions.DivActionTypedHandlerProxy;
import com.yandex.div.core.annotations.PublicApi;
import com.yandex.div.core.downloader.DivDownloadActionHandler;
import com.yandex.div.core.expression.storedvalues.StoredValuesActionHandler;
import com.yandex.div.core.state.DivStatePath;
import com.yandex.div.core.state.PathFormatException;
import com.yandex.div.core.view2.BindingContext;
import com.yandex.div.core.view2.Div2View;
import com.yandex.div.core.view2.ViewLocator;
import com.yandex.div.core.view2.divs.widgets.DivHolderView;
import com.yandex.div.core.view2.items.DivItemChangeActionHandler;
import com.yandex.div.data.VariableMutationException;
import com.yandex.div.internal.Assert;
@@ -43,6 +46,8 @@ public class DivActionHandler {
public static final String TIMER = "timer";
public static final String TRIGGER = "trigger";
public static final String VIDEO = "video";
public static final String ANIMATION_END = "animation_end";
public static final String ANIMATION_CANCEL = "animation_cancel";
}
private static final String SCHEME_DIV_ACTION = "div-action";
@@ -95,14 +100,17 @@ public class DivActionHandler {
@NonNull DivViewFacade view,
@NonNull ExpressionResolver resolver
) {
if (DivActionTypedHandlerProxy.handleAction(action, view, resolver)) {
ExpressionResolver scopedResolver = findExpressionResolverById((Div2View) view, action.scopeId);
ExpressionResolver localResolver = scopedResolver == null ? resolver : scopedResolver;
if (DivActionTypedHandlerProxy.handleAction(action, view, localResolver)) {
return true;
}
Uri url = action.url != null ? action.url.evaluate(resolver) : null;
if (DivDownloadActionHandler.canHandle(url, view)) {
return DivDownloadActionHandler.handleAction(action, (Div2View) view, resolver);
return DivDownloadActionHandler.handleAction(action, (Div2View) view, localResolver);
}
return handleActionUrl(url, view, resolver);
return handleAction(action.scopeId, url, view, localResolver);
}
/**
@@ -220,14 +228,17 @@ public class DivActionHandler {
@NonNull DivViewFacade view,
@NonNull ExpressionResolver resolver
) {
if (DivActionTypedHandlerProxy.handleVisibilityAction(action, view, resolver)) {
ExpressionResolver scopedResolver = findExpressionResolverById((Div2View) view, action.getScopeId());
ExpressionResolver localResolver = scopedResolver == null ? resolver : scopedResolver;
if (DivActionTypedHandlerProxy.handleVisibilityAction(action, view, localResolver)) {
return true;
}
Uri url = action.getUrl() != null ? action.getUrl().evaluate(resolver) : null;
if (DivDownloadActionHandler.canHandle(url, view)) {
return DivDownloadActionHandler.handleVisibilityAction(action, (Div2View) view, resolver);
return DivDownloadActionHandler.handleVisibilityAction(action, (Div2View) view, localResolver);
}
return handleActionUrl(url, view, resolver);
return handleAction(action.getScopeId(), url, view, resolver);
}
/**
@@ -325,6 +336,35 @@ public class DivActionHandler {
@Nullable Uri uri,
@NonNull DivViewFacade view,
@NonNull ExpressionResolver resolver
) {
return handleActionUrl(null, uri, view, resolver);
}
/**
* Handles the URI with {@code div-action} scheme.
*
* @param scopeId id of div that denotes scope of given action
* @param uri URI to handle
* @param view calling DivView
* @param resolver resolver for current action
* @return TRUE if uri was handled
*/
public final boolean handleActionUrl(
@Nullable String scopeId,
@Nullable Uri uri,
@NonNull DivViewFacade view,
@NonNull ExpressionResolver resolver
) {
ExpressionResolver scopedResolver = findExpressionResolverById((Div2View) view, scopeId);
ExpressionResolver localResolver = scopedResolver == null ? resolver : scopedResolver;
return handleAction(scopeId, uri, view, localResolver);
}
private boolean handleAction(
@Nullable String scopeId,
@Nullable Uri uri,
@NonNull DivViewFacade view,
@NonNull ExpressionResolver resolver
) {
if (uri == null) {
return false;
@@ -332,13 +372,18 @@ public class DivActionHandler {
//noinspection SimplifiableIfStatement
if (SCHEME_DIV_ACTION.equals(uri.getScheme())) {
return handleAction(uri, view, resolver);
return handleActionInternal(scopeId, uri, view, resolver);
}
return false;
}
private boolean handleAction(@NonNull Uri uri, @NonNull DivViewFacade view, @NonNull ExpressionResolver resolver) {
private boolean handleActionInternal(
@Nullable String scopeId,
@Nullable Uri uri,
@NonNull DivViewFacade view,
@NonNull ExpressionResolver resolver
) {
String action = uri.getAuthority();
if (AUTHORITY_SWITCH_STATE.equals(action)) {
String stateId = uri.getQueryParameter(PARAM_STATE_ID);
@@ -447,4 +492,20 @@ public class DivActionHandler {
return false;
}
@Nullable
private static ExpressionResolver findExpressionResolverById(Div2View divView, @Nullable String id) {
if (id == null) {
return null;
}
View targetView = ViewLocator.findViewWithTag(divView, id);
if (targetView instanceof DivHolderView) {
BindingContext bindingContext = ((DivHolderView<?>) targetView).getBindingContext();
if (bindingContext != null) {
return bindingContext.getExpressionResolver();
}
}
return null;
}
}
@@ -18,6 +18,7 @@ internal class DivActionTypedArrayMutationHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver,
@@ -8,7 +8,13 @@ import javax.inject.Singleton
@Singleton
internal class DivActionTypedClearFocusHandler @Inject constructor() : DivActionTypedHandler {
override fun handleAction(action: DivActionTyped, view: Div2View, resolver: ExpressionResolver): Boolean {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver
): Boolean {
return when (action) {
is DivActionTyped.ClearFocus -> {
view.clearFocus()
@@ -17,6 +17,7 @@ internal class DivActionTypedCopyToClipboardHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver,
@@ -15,6 +15,7 @@ internal class DivActionTypedDictSetValueHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver,
@@ -12,7 +12,13 @@ import javax.inject.Singleton
@Singleton
internal class DivActionTypedFocusElementHandler @Inject constructor() : DivActionTypedHandler {
override fun handleAction(action: DivActionTyped, view: Div2View, resolver: ExpressionResolver): Boolean {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver
): Boolean {
return when (action) {
is DivActionTyped.FocusElement -> {
handleRequestFocus(action.value, view, resolver)
@@ -6,5 +6,5 @@ import com.yandex.div2.DivActionTyped
internal interface DivActionTypedHandler {
fun handleAction(action: DivActionTyped, view: Div2View, resolver: ExpressionResolver): Boolean
fun handleAction(scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver): Boolean
}
@@ -12,8 +12,13 @@ internal class DivActionTypedHandlerCombiner @Inject constructor(
private val handlers: Set<@JvmSuppressWildcards DivActionTypedHandler>
) {
fun handleAction(action: DivActionTyped, div2View: Div2View, resolver: ExpressionResolver): Boolean {
val wasHandled = handlers.find { it.handleAction(action, div2View, resolver) } != null
fun handleAction(
scopeId: String?,
action: DivActionTyped,
div2View: Div2View,
resolver: ExpressionResolver
): Boolean {
val wasHandled = handlers.find { it.handleAction(scopeId, action, div2View, resolver) } != null
if (!wasHandled) {
KLog.d(TAG) { "Unexpected ${action::class.java} was not handled" }
}
@@ -14,15 +14,16 @@ internal object DivActionTypedHandlerProxy {
@JvmStatic
fun handleVisibilityAction(action: DivSightAction, view: DivViewFacade, resolver: ExpressionResolver): Boolean {
return handleAction(action.typed, view, resolver, action.downloadCallbacks)
return handleAction(action.scopeId, action.typed, view, resolver, action.downloadCallbacks)
}
@JvmStatic
fun handleAction(action: DivAction, view: DivViewFacade, resolver: ExpressionResolver): Boolean {
return handleAction(action.typed, view, resolver, action.downloadCallbacks)
return handleAction(action.scopeId, action.typed, view, resolver, action.downloadCallbacks)
}
private fun handleAction(
scopeId: String?,
action: DivActionTyped?,
view: DivViewFacade,
resolver: ExpressionResolver,
@@ -38,6 +39,7 @@ internal object DivActionTypedHandlerProxy {
if (action is DivActionTyped.Download) {
return DivDownloadActionHandler.handleAction(action.value, downloadCallbacks, view, resolver)
}
return view.div2Component.actionTypedHandlerCombiner.handleAction(action, view, resolver)
return view.div2Component.actionTypedHandlerCombiner.handleAction(scopeId, action, view, resolver)
}
}
@@ -8,10 +8,11 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DivActionTypedHideTooltipHandler @Inject constructor()
internal class DivActionTypedHideTooltipHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver
@@ -14,6 +14,7 @@ internal class DivActionTypedSetStateHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver
@@ -17,6 +17,7 @@ internal class DivActionTypedSetVariableHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver,
@@ -8,10 +8,11 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DivActionTypedShowTooltipHandler @Inject constructor()
internal class DivActionTypedShowTooltipHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver
@@ -8,9 +8,10 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DivActionTypedTimerHandler @Inject constructor()
internal class DivActionTypedTimerHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver
@@ -8,10 +8,11 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DivActionTypedVideoHandler @Inject constructor()
internal class DivActionTypedVideoHandler @Inject constructor()
: DivActionTypedHandler {
override fun handleAction(
scopeId: String?,
action: DivActionTyped,
view: Div2View,
resolver: ExpressionResolver
@@ -0,0 +1,41 @@
package com.yandex.div.core.view2
import android.view.View
import android.view.ViewGroup
import com.yandex.div.core.actions.logError
internal object ViewLocator {
@JvmStatic
fun findViewWithTag(divView: Div2View, tag: String): View? {
val foundViews = divView.view.findViewsWithTag(tag)
if (foundViews.isEmpty()) {
return null
}
if (foundViews.size > 1) {
divView.logError(RuntimeException("Ambiguous scope id. There are ${foundViews.size} divs with id '$tag'"))
return null
}
return foundViews.first()
}
private fun View.findViewsWithTag(tag: Any?): List<View> {
if (tag == null) return emptyList()
val result = mutableListOf<View>()
findViewsWithTagTraversal(this, tag, result)
return result
}
private fun findViewsWithTagTraversal(view: View, tag: Any, views: MutableList<View>): List<View> {
if (tag == view.tag) {
views += view
}
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
findViewsWithTagTraversal(view.getChildAt(i), tag, views)
}
}
return views
}
}
@@ -0,0 +1,95 @@
package com.yandex.div
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.rule.ActivityTestRule
import com.yandex.div.rule.uiTestRule
import com.yandex.div.steps.DivViewInteractions.viewWithTag
import com.yandex.div.steps.DivViewInteractions.viewWithTagAndText
import com.yandex.div.steps.divView
import com.yandex.div.view.isDisplayed
import com.yandex.divkit.demo.DummyActivity
import com.yandex.divkit.demo.div.DemoDiv2Logger
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class DivActionTest {
private val activityRule = ActivityTestRule(DummyActivity::class.java)
@get:Rule
val rule = uiTestRule { activityRule }
@Before
fun clearLogs() {
DemoDiv2Logger.clearLogActions()
}
@After
fun unregisterAllResources() {
val resources: Collection<IdlingResource> = IdlingRegistry.getInstance().resources
if (!resources.isEmpty()) {
resources.forEach { IdlingRegistry.getInstance().unregister(it) }
}
}
@Test
fun scopedActionChangesLocalVariable() {
divView {
testAsset = "ui_test_data/actions/scoped_actions.json"
activityRule.buildContainer(MATCH_PARENT, WRAP_CONTENT)
tapOnText("Scoped")
assert {
viewWithTag(tag = "title").checkHasText(text = "Title has been changed")
}
}
}
@Test
fun notScopedActionDoesNotChangeLocalVariable() {
divView {
testAsset = "ui_test_data/actions/scoped_actions.json"
activityRule.buildContainer(MATCH_PARENT, WRAP_CONTENT)
tapOnText("Not scoped")
assert {
viewWithTag(tag = "title").checkHasText(text = "Lorem ipsum")
}
}
}
@Test
fun scopedActionChangesLocalVariable_fromNestedScope() {
divView {
testAsset = "ui_test_data/actions/scoped_actions_nested_scope.json"
activityRule.buildContainer(MATCH_PARENT, WRAP_CONTENT)
tapOnText("Content scope")
assert {
viewWithTag(tag = "title").checkHasText(text = "Title has been changed from 'content' scope")
}
}
}
@Test
fun scopedActionDoesNotChangeLocalVariable_whenScopeIsAmbiguous() {
divView {
testAsset = "ui_test_data/actions/scoped_actions_ambiguous_scope.json"
activityRule.buildContainer(MATCH_PARENT, WRAP_CONTENT)
tapOnText("Ambiguous scope")
assert {
viewWithTagAndText(tag = "some_text", text = "Lorem ipsum").isDisplayed()
}
}
}
}
@@ -1,8 +1,21 @@
package com.yandex.div.steps
import android.view.ViewGroup
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.yandex.div.core.Div2Context
import com.yandex.div.core.view2.Div2View
import com.yandex.test.util.Report.step
import com.yandex.test.util.StepsDsl
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.`is`
private val divViewMatcher = isAssignableFrom(Div2View::class.java)
internal fun divView(block: DivViewSteps.() -> Unit) = block(DivViewSteps())
@@ -28,4 +41,29 @@ class DivViewSteps: DivTestAssetSteps() {
fun detachFromParent() = runOnMainSync {
container.removeView(div2View)
}
fun tapOnText(text: String): Unit = step("Click on text '$text'") {
onView(withText(text)).perform(click())
}
fun assert(f: DivViewAssertions.() -> Unit) = f(DivViewAssertions())
}
object DivViewInteractions {
fun viewWithTag(tag: String): ViewInteraction {
return onView(withTagValue(`is`(tag)))
}
fun viewWithTagAndText(tag: String, text: String): ViewInteraction {
return onView(allOf(withTagValue(`is`(tag)), withText(text)))
}
}
@StepsDsl
class DivViewAssertions {
fun ViewInteraction.checkHasText(text: String): Unit = step("Assert view has text '$text'") {
check(ViewAssertions.matches(withText(text)))
}
}
@@ -0,0 +1,133 @@
{
"templates": {
"title": {
"type": "text",
"font_size": 24,
"font_weight": "bold",
"text_alignment_horizontal": "center",
"text_alignment_vertical": "center",
"text_color": "#586E75"
},
"base_text": {
"type": "text",
"font_size": 18,
"font_weight": "medium",
"text_color": "#002B36"
},
"button": {
"type": "text",
"height": {
"type": "fixed",
"value": 48
},
"paddings": {
"left": 8,
"top": 8,
"right": 8,
"bottom": 8
},
"background": [
{
"type": "solid",
"$color": "background_color"
}
],
"border": {
"corner_radius": 8
},
"font_size": 20,
"font_weight": "medium",
"text_alignment_horizontal": "center",
"text_alignment_vertical": "center",
"text_color": "#FDF6E3"
}
},
"card": {
"log_id": "scoped_actions",
"states": [
{
"state_id": 0,
"div": {
"type": "container",
"orientation": "vertical",
"paddings": {
"left": 16,
"top": 16,
"right": 16,
"bottom": 16
},
"background": [
{
"type": "solid",
"color": "#EEE8D5"
}
],
"items": [
{
"type": "title",
"id": "title",
"text": "@{title_text}",
"margins": {
"bottom": 16
},
"variables": [
{
"name": "title_text",
"type": "string",
"value": "Lorem ipsum"
}
]
},
{
"type": "base_text",
"id": "content",
"text": "@{content_text}",
"margins": {
"bottom": 16
},
"variables": [
{
"name": "content_text",
"type": "string",
"value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
}
]
},
{
"type": "container",
"orientation": "horizontal",
"items": [
{
"type": "button",
"margins": {
"right": 16
},
"background_color": "#2AA198",
"text": "Scoped",
"actions": [
{
"log_id": "change_title",
"scope_id": "title",
"url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed"
}
]
},
{
"type": "button",
"background_color": "#B58900",
"text": "Not scoped",
"actions": [
{
"log_id": "change_title",
"url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed"
}
]
}
]
}
]
}
}
]
}
}
@@ -0,0 +1,114 @@
{
"templates": {
"title": {
"type": "text",
"font_size": 24,
"font_weight": "bold",
"text_alignment_horizontal": "center",
"text_alignment_vertical": "center",
"text_color": "#586E75"
},
"base_text": {
"type": "text",
"font_size": 18,
"font_weight": "medium",
"text_color": "#002B36"
},
"button": {
"type": "text",
"height": {
"type": "fixed",
"value": 48
},
"paddings": {
"left": 8,
"top": 8,
"right": 8,
"bottom": 8
},
"background": [
{
"type": "solid",
"$color": "background_color"
}
],
"border": {
"corner_radius": 8
},
"font_size": 20,
"font_weight": "medium",
"text_alignment_horizontal": "center",
"text_alignment_vertical": "center",
"text_color": "#FDF6E3"
}
},
"card": {
"log_id": "scoped_actions_ambiguous_scope",
"states": [
{
"state_id": 0,
"div": {
"type": "container",
"id": "title",
"orientation": "vertical",
"paddings": {
"left": 16,
"top": 16,
"right": 16,
"bottom": 16
},
"background": [
{
"type": "solid",
"color": "#EEE8D5"
}
],
"items": [
{
"type": "title",
"id": "title",
"text": "@{title_text}",
"margins": {
"bottom": 16
},
"variables": [
{
"name": "title_text",
"type": "string",
"value": "Lorem ipsum"
}
]
},
{
"type": "base_text",
"id": "content",
"text": "@{content_text}",
"margins": {
"bottom": 16
},
"variables": [
{
"name": "content_text",
"type": "string",
"value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
}
]
},
{
"type": "button",
"background_color": "#B58900",
"text": "Ambiguous scope",
"actions": [
{
"log_id": "change_some_text",
"scope_id": "title",
"url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed"
}
]
}
]
}
}
]
}
}
@@ -0,0 +1,132 @@
{
"templates": {
"title": {
"type": "text",
"font_size": 24,
"font_weight": "bold",
"text_alignment_horizontal": "center",
"text_alignment_vertical": "center",
"text_color": "#586E75"
},
"base_text": {
"type": "text",
"font_size": 18,
"font_weight": "medium",
"text_color": "#002B36"
},
"button": {
"type": "text",
"height": {
"type": "fixed",
"value": 48
},
"paddings": {
"left": 8,
"top": 8,
"right": 8,
"bottom": 8
},
"background": [
{
"type": "solid",
"$color": "background_color"
}
],
"border": {
"corner_radius": 8
},
"font_size": 20,
"font_weight": "medium",
"text_alignment_horizontal": "center",
"text_alignment_vertical": "center",
"text_color": "#FDF6E3"
}
},
"card": {
"log_id": "scoped_actions_nested_scope",
"states": [
{
"state_id": 0,
"div": {
"type": "container",
"orientation": "vertical",
"paddings": {
"left": 16,
"top": 16,
"right": 16,
"bottom": 16
},
"background": [
{
"type": "solid",
"color": "#EEE8D5"
}
],
"items": [
{
"type": "title",
"id": "title",
"text": "@{title_text}",
"margins": {
"bottom": 16
}
},
{
"type": "base_text",
"id": "content",
"text": "@{content_text}",
"margins": {
"bottom": 16
}
},
{
"type": "container",
"orientation": "horizontal",
"items": [
{
"type": "button",
"margins": {
"right": 16
},
"background_color": "#2AA198",
"text": "Title scope",
"actions": [
{
"log_id": "change_title",
"scope_id": "title",
"url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed%20from%20'title'%20scope"
}
]
},
{
"type": "button",
"background_color": "#268BD2",
"text": "Content scope",
"actions": [
{
"log_id": "change_title",
"scope_id": "content",
"url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed%20from%20'content'%20scope"
}
]
}
]
}
],
"variables": [
{
"name": "title_text",
"type": "string",
"value": "Lorem ipsum"
},
{
"name": "content_text",
"type": "string",
"value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
}
]
}
}
]
}
}