Skip to content

Commit

Permalink
Implement Stack widget (#3269)
Browse files Browse the repository at this point in the history
* Implement a new stack widget.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Add background color opacity.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Adjust box background to make it configurable.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Add stack view test.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Interpolate child properties.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Add stack widget documentation

* Update stack widget doc formatting

* Update ViewType.kt

---------

Signed-off-by: Lentumunai-Mark <[email protected]>
Co-authored-by: Benjamin Mwalimu <[email protected]>
  • Loading branch information
Lentumunai-Mark and dubdabasoduba authored May 28, 2024
1 parent 1d82052 commit 7057c38
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.configuration.view

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import org.smartregister.fhircore.engine.domain.model.ViewType
import org.smartregister.fhircore.engine.util.extension.interpolate

@Serializable
@Parcelize
data class StackViewProperties(
override val viewType: ViewType = ViewType.STACK,
override val weight: Float = 0f,
override val backgroundColor: String? = "#FFFFFF",
override val padding: Int = 0,
override val borderRadius: Int = 0,
override val alignment: ViewAlignment = ViewAlignment.NONE,
override val fillMaxWidth: Boolean = false,
override val fillMaxHeight: Boolean = false,
override val clickable: String = "false",
override val visible: String = "true",
val opacity: Float = 0f,
val size: Int? = 0,
val children: List<ViewProperties> = emptyList(),
) : ViewProperties(), Parcelable {
override fun interpolate(computedValuesMap: Map<String, Any>): StackViewProperties {
return this.copy(
backgroundColor = backgroundColor?.interpolate(computedValuesMap),
visible = visible.interpolate(computedValuesMap),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@ enum class ViewAlignment {
END,
CENTER,
NONE,
TOPSTART,
TOPEND,
BOTTOMSTART,
BOTTOMEND,
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ object ViewPropertiesSerializer :
ViewType.LIST -> ListProperties.serializer()
ViewType.IMAGE -> ImageProperties.serializer()
ViewType.BORDER -> DividerProperties.serializer()
ViewType.STACK -> StackViewProperties.serializer()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ enum class ViewType {

/** A type of view component used to render divider between views */
@JsonNames("border", "Border") BORDER,

/** A type of view component used to overlay different views */
@JsonNames("stack", "Stack") STACK,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.quest.integration.ui.shared.components

import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.navigation.compose.rememberNavController
import org.hl7.fhir.r4.model.ResourceType
import org.junit.Rule
import org.junit.Test
import org.smartregister.fhircore.engine.configuration.view.StackViewProperties
import org.smartregister.fhircore.engine.configuration.view.ViewAlignment
import org.smartregister.fhircore.engine.domain.model.ResourceData
import org.smartregister.fhircore.quest.ui.shared.components.STACK_VIEW_TEST_TAG
import org.smartregister.fhircore.quest.ui.shared.components.StackView

class StackViewTest {
@get:Rule val composeTestRule = createComposeRule()

@Test
fun testStackViewIsRendered() {
val stackViewProperties = StackViewProperties(alignment = ViewAlignment.CENTER, size = 250)
composeTestRule.setContent {
StackView(
modifier = Modifier,
stackViewProperties = stackViewProperties,
resourceData = ResourceData("", ResourceType.Patient, emptyMap()),
navController = rememberNavController(),
)
}
composeTestRule.onNodeWithTag(STACK_VIEW_TEST_TAG).assertExists()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.smartregister.fhircore.engine.configuration.view.PersonalDataProperti
import org.smartregister.fhircore.engine.configuration.view.RowArrangement
import org.smartregister.fhircore.engine.configuration.view.RowProperties
import org.smartregister.fhircore.engine.configuration.view.ServiceCardProperties
import org.smartregister.fhircore.engine.configuration.view.StackViewProperties
import org.smartregister.fhircore.engine.configuration.view.ViewAlignment
import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger
import org.smartregister.fhircore.engine.configuration.workflow.ApplicationWorkflow
Expand Down Expand Up @@ -386,4 +387,35 @@ class ViewGeneratorTest {
.assertExists()
.assertIsDisplayed()
}

@Test
fun testStackViewIsRenderedCorrectlyWhenStackViewPropertiesHasChildren() {
composeRule.setContent {
GenerateView(
properties =
StackViewProperties(
size = 250,
alignment = ViewAlignment.CENTER,
children =
listOf(
ButtonProperties(status = "DUE", text = "Due Task"),
ButtonProperties(status = "COMPLETED", text = "Completed Task"),
ButtonProperties(status = "READY", text = "Ready Task"),
),
viewType = ViewType.STACK,
),
resourceData = resourceData,
navController = TestNavHostController(LocalContext.current),
)
}
composeRule
.onNodeWithText("Due Task", useUnmergedTree = true)
.assertExists()
.assertIsDisplayed()
composeRule.onNodeWithText("Completed Task", useUnmergedTree = true).assertExists()
composeRule
.onNodeWithText("Ready Task", useUnmergedTree = true)
.assertExists()
.assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.quest.ui.shared.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.configuration.view.StackViewProperties
import org.smartregister.fhircore.engine.configuration.view.ViewAlignment
import org.smartregister.fhircore.engine.domain.model.ResourceData
import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated
import org.smartregister.fhircore.engine.util.extension.parseColor

const val STACK_VIEW_TEST_TAG = "stackViewTestTag"

@Composable
fun StackView(
modifier: Modifier,
stackViewProperties: StackViewProperties,
resourceData: ResourceData,
navController: NavController,
) {
val backgroundColor = stackViewProperties.backgroundColor.parseColor()
val size = stackViewProperties.size

Box(
modifier =
Modifier.background(backgroundColor.copy(alpha = stackViewProperties.opacity))
.size(size!!.dp)
.testTag(STACK_VIEW_TEST_TAG),
contentAlignment = castViewAlignment(stackViewProperties.alignment),
) {
stackViewProperties.children.forEach { child ->
GenerateView(
modifier = generateModifier(viewProperties = child),
properties = child.interpolate(resourceData.computedValuesMap),
resourceData = resourceData,
navController = navController,
)
}
}
}

fun castViewAlignment(
viewAlignment: ViewAlignment,
): Alignment {
return when (viewAlignment) {
ViewAlignment.TOPSTART -> Alignment.TopStart
ViewAlignment.TOPEND -> Alignment.TopEnd
ViewAlignment.CENTER -> Alignment.Center
ViewAlignment.BOTTOMSTART -> Alignment.BottomStart
ViewAlignment.BOTTOMEND -> Alignment.BottomEnd
else -> {
Alignment.Center
}
}
}

@PreviewWithBackgroundExcludeGenerated
@Composable
private fun PreviewStack() {
StackView(
stackViewProperties =
StackViewProperties(
opacity = 0.2f,
size = 250,
backgroundColor = "successColor",
),
modifier = Modifier,
navController = rememberNavController(),
resourceData =
ResourceData(
baseResourceId = "baseId",
baseResourceType = ResourceType.Patient,
computedValuesMap = emptyMap(),
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import org.smartregister.fhircore.engine.configuration.view.PersonalDataProperti
import org.smartregister.fhircore.engine.configuration.view.RowProperties
import org.smartregister.fhircore.engine.configuration.view.ServiceCardProperties
import org.smartregister.fhircore.engine.configuration.view.SpacerProperties
import org.smartregister.fhircore.engine.configuration.view.StackViewProperties
import org.smartregister.fhircore.engine.configuration.view.ViewAlignment
import org.smartregister.fhircore.engine.configuration.view.ViewProperties
import org.smartregister.fhircore.engine.domain.model.ResourceData
Expand Down Expand Up @@ -119,6 +120,9 @@ fun GenerateView(
ViewAlignment.END -> Alignment.End
ViewAlignment.CENTER -> Alignment.CenterHorizontally
ViewAlignment.NONE -> Alignment.Start
else -> {
Alignment.Start
}
},
modifier =
modifier
Expand Down Expand Up @@ -191,6 +195,9 @@ fun GenerateView(
ViewAlignment.END -> Alignment.Bottom
ViewAlignment.CENTER -> Alignment.CenterVertically
ViewAlignment.NONE -> Alignment.CenterVertically
else -> {
Alignment.CenterVertically
}
},
modifier =
modifier
Expand Down Expand Up @@ -262,6 +269,13 @@ fun GenerateView(
resourceData = resourceData,
navController = navController,
)
ViewType.STACK ->
StackView(
modifier = modifier,
stackViewProperties = properties as StackViewProperties,
resourceData = resourceData,
navController = navController,
)
}
}
}
Expand Down
60 changes: 60 additions & 0 deletions docs/engineering/android-app/configuring/config-types/widget.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,66 @@ height | The height of the Spacer. | Yes | _ |
backgroundColor | The background color of the view, specified as a string in the format "#RRGGBB" or "#AARRGGBB". If this property is null, the view will use its parent's background color. | No | #FFFFFF |
padding | Offsets the content of the view by a specific number of pixels. This should be a number | No | 0 |

## Stack view widget
The Stack View Widget can hold a list of child views, allowing you to arrange them one on top of the other.
The Stack View acts as a container that vertically stacks its child views. It provides a way to create layered layouts where elements can be positioned on top of each other. This makes it ideal for use cases like:
- Overlaying informational elements like badges or icons on top of content.
- Creating progress indicators or loaders that sit on top of the main view.
- Implementing toasts or pop-up notifications that appear above the primary content.
### Example JSON:
```json
{
"viewType": "STACK",
"backgroundColor": "successColor",
"size": 100,
"opacity": 0.1,
"children": [
{
"viewType": "IMAGE",
"alignment": "CENTER",
"size": 90,
"imageConfig": {
"type": "local",
"reference": "ic_main_image"
},
"isCircular": false
},
{
"viewType": "IMAGE",
"size": 38,
"visible": "true",
"imageConfig": {
"type": "local",
"reference": "ic_alert_triangle"
},
"isCircular": true
}
]
}
```
### Config properties of STACK VIEW WIDGET
The Stack View Widget inherits common properties from the ViewProperties class. These properties allow you to configure the general appearance and behavior of the stack, including:
- weight
- backgroundColor
- padding
- borderRadius
- alignment
- fillMaxWidth
- fillMaxHeight
- clickable
- visible

Stack view specific properties include :

|Property | Description | Required | Default |
|--|--|:--:|:--:|
viewType | Specifies the type of view. In this case, it's always `STACK` to identify it during runtime | Yes | [STACK]|
backgroundColor | this represents the background color of the container view, specified as a color code in the format "#RRGGBB" or "#AARRGGBB". If null, it indicates a transparent background. | | String? |
opacity | Opacity of the view, ranging from 0.0 (fully transparent) to 1.0 (fully opaque).| No | Float |
size | Height of the stack view in pixels. If not set, the stack will wrap its content | No | Int |
children | List of child view definitions. Each child view definition should be a valid JSON object representing a supported view type (e.g., COMPUNDTEXT, IMAGE, BUTTON)..| | List |


## Personal Data Widgets
Personal data widget, defines personal data like age to it's corresponding value in a more organised way.It takes a list of mapped items as input.It contains various properties that defines its behavior.

Expand Down

0 comments on commit 7057c38

Please sign in to comment.