Skip to content

Commit 2d82cdb

Browse files
committed
Merge branch 'main' into improve-sync-progress-ux
2 parents bc85147 + 6fc15b0 commit 2d82cdb

File tree

39 files changed

+908
-599
lines changed

39 files changed

+908
-599
lines changed

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import org.smartregister.fhircore.engine.configuration.app.ApplicationConfigurat
5858
import org.smartregister.fhircore.engine.configuration.app.ConfigService
5959
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource
6060
import org.smartregister.fhircore.engine.di.NetworkModule
61+
import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction
6162
import org.smartregister.fhircore.engine.util.DispatcherProvider
6263
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
6364
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
@@ -72,7 +73,7 @@ import org.smartregister.fhircore.engine.util.extension.generateMissingId
7273
import org.smartregister.fhircore.engine.util.extension.interpolate
7374
import org.smartregister.fhircore.engine.util.extension.referenceValue
7475
import org.smartregister.fhircore.engine.util.extension.retrieveCompositionSections
75-
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationIds
76+
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState
7677
import org.smartregister.fhircore.engine.util.extension.searchCompositionByIdentifier
7778
import org.smartregister.fhircore.engine.util.extension.updateLastUpdated
7879
import org.smartregister.fhircore.engine.util.helper.LocalizationHelper
@@ -743,7 +744,10 @@ constructor(
743744
configService.defineResourceTags().find { it.type == ResourceType.Organization.name }
744745
val mandatoryTags = configService.provideResourceTags(sharedPreferencesHelper)
745746

746-
val locationIds = context.retrieveRelatedEntitySyncLocationIds()
747+
val locationIds =
748+
context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.SYNC_DATA).map {
749+
it.locationId
750+
}
747751

748752
syncConfig.parameter
749753
.map { it.resource as SearchParameter }

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ImageProperties.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ data class ImageProperties(
4444
val tint: String? = null,
4545
val text: String? = null,
4646
val imageConfig: ImageConfig? = null,
47-
val size: Int? = null,
47+
val size: Int? = 22,
4848
val shape: ImageShape? = null,
4949
val textColor: String? = null,
5050
val actions: List<ActionConfig> = emptyList(),

android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFi
7575
import org.smartregister.fhircore.engine.domain.model.Code
7676
import org.smartregister.fhircore.engine.domain.model.DataQuery
7777
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig
78+
import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction
7879
import org.smartregister.fhircore.engine.domain.model.RelatedResourceCount
7980
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
8081
import org.smartregister.fhircore.engine.domain.model.ResourceConfig
@@ -93,7 +94,7 @@ import org.smartregister.fhircore.engine.util.extension.filterBy
9394
import org.smartregister.fhircore.engine.util.extension.filterByResourceTypeId
9495
import org.smartregister.fhircore.engine.util.extension.generateMissingId
9596
import org.smartregister.fhircore.engine.util.extension.loadResource
96-
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationIds
97+
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState
9798
import org.smartregister.fhircore.engine.util.extension.updateFrom
9899
import org.smartregister.fhircore.engine.util.extension.updateLastUpdated
99100
import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor
@@ -934,7 +935,11 @@ constructor(
934935
configComputedRuleValues: Map<String, Any>,
935936
) =
936937
if (filterByRelatedEntityLocation) {
937-
val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds()
938+
val syncLocationIds =
939+
context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.FILTER_DATA).map {
940+
it.locationId
941+
}
942+
938943
val locationIds =
939944
syncLocationIds
940945
.map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } }
@@ -1017,7 +1022,10 @@ constructor(
10171022
val configComputedRuleValues = configRules.configRulesComputedValues()
10181023

10191024
if (filterByRelatedEntityLocationMetaTag) {
1020-
val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds()
1025+
val syncLocationIds =
1026+
context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.FILTER_DATA).map {
1027+
it.locationId
1028+
}
10211029
val locationIds =
10221030
syncLocationIds
10231031
.map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } }

android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/ProtoDataStore.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,9 @@ import timber.log.Timber
3636

3737
private const val PRACTITIONER_DETAILS_DATASTORE_JSON = "practitioner_details.json"
3838
private const val USER_INFO_DATASTORE_JSON = "user_info.json"
39-
4039
private const val LOCATION_COORDINATES_DATASTORE_JSON = "location_coordinates.json"
41-
4240
private const val SYNC_LOCATION_IDS = "sync_location_ids.json"
41+
private const val DATA_FILTER_LOCATION_IDS = "data_filter_location_ids.json"
4342

4443
val Context.practitionerProtoStore: DataStore<PractitionerDetails> by
4544
dataStore(
@@ -64,6 +63,11 @@ val Context.syncLocationIdsProtoStore: DataStore<Map<String, SyncLocationState>>
6463
fileName = SYNC_LOCATION_IDS,
6564
serializer = SyncLocationIdDataStoreSerializer,
6665
)
66+
val Context.dataFilterLocationIdsProtoStore: DataStore<Map<String, SyncLocationState>> by
67+
dataStore(
68+
fileName = DATA_FILTER_LOCATION_IDS,
69+
serializer = SyncLocationIdDataStoreSerializer,
70+
)
6771

6872
@Singleton
6973
class ProtoDataStore @Inject constructor(@ApplicationContext val context: Context) {

android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/MultiSelectViewConfig.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import kotlinx.serialization.Serializable
2929
* @property rootNodeFhirPathExpression A key value pair containing a FHIRPath expression for
3030
* extracting the value used to identify if the current resource is Root. The key is the FHIRPath
3131
* expression while value is the content to compare against.
32+
* @property viewActions The actions to be performed when the multiselect action button is pressed
33+
* @property mutuallyExclusive Setup the multi choice checkbox such that only a single (root level)
34+
* selection can be performed at a time.
3235
*/
3336
@Serializable
3437
@Parcelize
@@ -37,4 +40,11 @@ data class MultiSelectViewConfig(
3740
val parentIdFhirPathExpression: String,
3841
val contentFhirPathExpression: String,
3942
val rootNodeFhirPathExpression: KeyValueConfig,
43+
val viewActions: List<MultiSelectViewAction> = listOf(MultiSelectViewAction.FILTER_DATA),
44+
val mutuallyExclusive: Boolean = true,
4045
) : java.io.Serializable, Parcelable
46+
47+
enum class MultiSelectViewAction {
48+
SYNC_DATA,
49+
FILTER_DATA,
50+
}

android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ fun <T> ColumnScope.MultiSelectView(
4646
rootTreeNode: TreeNode<T>,
4747
syncLocationStateMap: MutableMap<String, SyncLocationState>,
4848
depth: Int = 0,
49+
onChecked: () -> Unit,
4950
content: @Composable (TreeNode<T>) -> Unit,
5051
) {
5152
val collapsedState = remember { mutableStateOf(false) }
@@ -55,6 +56,7 @@ fun <T> ColumnScope.MultiSelectView(
5556
depth = depth,
5657
content = content,
5758
collapsedState = collapsedState,
59+
onChecked = onChecked,
5860
)
5961
if (collapsedState.value) {
6062
rootTreeNode.children.forEach {
@@ -63,18 +65,20 @@ fun <T> ColumnScope.MultiSelectView(
6365
syncLocationStateMap = syncLocationStateMap,
6466
depth = depth + 16,
6567
content = content,
68+
onChecked = onChecked,
6669
)
6770
}
6871
}
6972
}
7073

7174
@Composable
72-
fun <T> MultiSelectCheckbox(
75+
private fun <T> MultiSelectCheckbox(
7376
syncLocationStateMap: MutableMap<String, SyncLocationState>,
7477
currentTreeNode: TreeNode<T>,
7578
depth: Int,
7679
content: @Composable (TreeNode<T>) -> Unit,
7780
collapsedState: MutableState<Boolean>,
81+
onChecked: () -> Unit,
7882
) {
7983
val checked = remember { mutableStateOf(false) }
8084
Column {
@@ -134,19 +138,12 @@ fun <T> MultiSelectCheckbox(
134138
parent = parent.parent
135139
}
136140

137-
// Select all the nested checkboxes
138-
val treeNodeArrayDeque = ArrayDeque(currentTreeNode.children)
139-
140-
while (treeNodeArrayDeque.isNotEmpty()) {
141-
val currentNode = treeNodeArrayDeque.removeFirst()
142-
syncLocationStateMap[currentNode.id] =
143-
SyncLocationState(
144-
currentNode.id,
145-
currentNode.parent?.id,
146-
ToggleableState(checked.value),
147-
)
148-
currentNode.children.forEach { treeNodeArrayDeque.addLast(it) }
149-
}
141+
updateNestedCheckboxState(
142+
currentTreeNode = currentTreeNode,
143+
syncLocationStateMap = syncLocationStateMap,
144+
checked = checked.value,
145+
)
146+
onChecked()
150147
},
151148
modifier = Modifier.padding(0.dp),
152149
colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.primary),
@@ -156,3 +153,26 @@ fun <T> MultiSelectCheckbox(
156153
}
157154
}
158155
}
156+
157+
/**
158+
* This function selects/deselects all the children for the [currentTreeNode] based on the value for
159+
* the [checked] parameter. The states for the [MultiSelectCheckbox] is updated in the
160+
* [syncLocationStateMap].
161+
*/
162+
fun <T> updateNestedCheckboxState(
163+
currentTreeNode: TreeNode<T>,
164+
syncLocationStateMap: MutableMap<String, SyncLocationState>,
165+
checked: Boolean,
166+
) {
167+
val treeNodeArrayDeque = ArrayDeque(currentTreeNode.children)
168+
while (treeNodeArrayDeque.isNotEmpty()) {
169+
val currentNode = treeNodeArrayDeque.removeFirst()
170+
syncLocationStateMap[currentNode.id] =
171+
SyncLocationState(
172+
locationId = currentNode.id,
173+
parentLocationId = currentNode.parent?.id,
174+
toggleableState = ToggleableState(checked),
175+
)
176+
currentNode.children.forEach { treeNodeArrayDeque.addLast(it) }
177+
}
178+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2021-2024 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.util
18+
19+
import org.hl7.fhir.r4.formats.JsonParser
20+
import org.hl7.fhir.r4.model.BooleanType
21+
import org.hl7.fhir.r4.model.DateTimeType
22+
import org.hl7.fhir.r4.model.DateType
23+
import org.hl7.fhir.r4.model.DecimalType
24+
import org.hl7.fhir.r4.model.Enumerations.DataType
25+
import org.hl7.fhir.r4.model.IntegerType
26+
import org.hl7.fhir.r4.model.StringType
27+
import org.hl7.fhir.r4.model.TimeType
28+
import org.hl7.fhir.r4.model.Type
29+
import org.hl7.fhir.r4.model.UriType
30+
import org.hl7.fhir.r4.utils.TypesUtilities
31+
import timber.log.Timber
32+
33+
private val fhirTypesJsonParser: JsonParser = JsonParser()
34+
35+
private fun String.castToFhirPrimitiveType(type: DataType): Type? =
36+
when (type) {
37+
DataType.BOOLEAN -> BooleanType(this)
38+
DataType.DECIMAL -> DecimalType(this)
39+
DataType.INTEGER -> IntegerType(this)
40+
DataType.DATE -> DateType(this)
41+
DataType.DATETIME -> DateTimeType(this)
42+
DataType.TIME -> TimeType(this)
43+
DataType.STRING -> StringType(this)
44+
DataType.URI -> UriType(this)
45+
else -> null
46+
}
47+
48+
private fun String.castToFhirDataType(type: DataType): Type? =
49+
try {
50+
fhirTypesJsonParser.parseType(this, type.toCode())
51+
} catch (ex: Exception) {
52+
Timber.e("Error casting \"$this\" to FHIR type \"${type.toCode()}\"")
53+
throw ex
54+
}
55+
56+
/** Cast string value (including json string) to the FHIR {@link org.hl7.fhir.r4.model.Type} */
57+
fun String.castToType(type: DataType): Type? {
58+
return if (TypesUtilities.isPrimitive(type.toCode())) {
59+
castToFhirPrimitiveType(type)
60+
} else {
61+
castToFhirDataType(type)
62+
}
63+
}

android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@ import androidx.core.view.WindowInsetsCompat
3939
import androidx.core.view.updatePadding
4040
import java.io.Serializable
4141
import java.util.Locale
42+
import kotlinx.coroutines.Dispatchers
4243
import kotlinx.coroutines.flow.firstOrNull
44+
import kotlinx.coroutines.withContext
45+
import org.smartregister.fhircore.engine.datastore.dataFilterLocationIdsProtoStore
4346
import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore
47+
import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction
48+
import org.smartregister.fhircore.engine.domain.model.SyncLocationState
4449
import org.smartregister.fhircore.engine.ui.theme.DangerColor
4550
import org.smartregister.fhircore.engine.ui.theme.DefaultColor
4651
import org.smartregister.fhircore.engine.ui.theme.InfoColor
@@ -221,13 +226,25 @@ inline fun <reified T : Parcelable> Intent.parcelableArrayList(key: String): Arr
221226
else -> @Suppress("DEPRECATION") getParcelableArrayListExtra(key)
222227
}
223228

224-
suspend fun Context.retrieveRelatedEntitySyncLocationIds(): List<String> {
225-
val selectedLocationStateMap = this.syncLocationIdsProtoStore.data.firstOrNull()
226-
return selectedLocationStateMap
227-
?.values
228-
?.filter {
229+
suspend fun Context.retrieveRelatedEntitySyncLocationState(
230+
multiSelectViewAction: MultiSelectViewAction,
231+
filterToggleableStateOn: Boolean = true,
232+
): List<SyncLocationState> {
233+
val selectedLocationStateMap =
234+
withContext(Dispatchers.IO) {
235+
val context = this@retrieveRelatedEntitySyncLocationState
236+
when (multiSelectViewAction) {
237+
MultiSelectViewAction.SYNC_DATA -> context.syncLocationIdsProtoStore.data.firstOrNull()
238+
MultiSelectViewAction.FILTER_DATA ->
239+
context.dataFilterLocationIdsProtoStore.data.firstOrNull()
240+
}
241+
}
242+
return if (filterToggleableStateOn) {
243+
selectedLocationStateMap?.values?.filter {
229244
it.toggleableState == ToggleableState.On &&
230245
selectedLocationStateMap[it.parentLocationId]?.toggleableState != ToggleableState.On
231246
}
232-
?.map { it.locationId } ?: emptyList()
247+
} else {
248+
selectedLocationStateMap?.values?.toList()
249+
} ?: emptyList()
233250
}

android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,13 @@ import com.google.android.fhir.datacapture.extensions.asStringValue
2020
import com.google.android.fhir.datacapture.extensions.logicalId
2121
import com.google.android.fhir.datacapture.extensions.targetStructureMap
2222
import java.util.Locale
23-
import org.hl7.fhir.r4.model.BooleanType
2423
import org.hl7.fhir.r4.model.Bundle
2524
import org.hl7.fhir.r4.model.Coding
26-
import org.hl7.fhir.r4.model.DateTimeType
27-
import org.hl7.fhir.r4.model.DateType
28-
import org.hl7.fhir.r4.model.DecimalType
29-
import org.hl7.fhir.r4.model.Enumerations.DataType
3025
import org.hl7.fhir.r4.model.Expression
3126
import org.hl7.fhir.r4.model.IdType
32-
import org.hl7.fhir.r4.model.IntegerType
33-
import org.hl7.fhir.r4.model.Quantity
3427
import org.hl7.fhir.r4.model.Questionnaire
3528
import org.hl7.fhir.r4.model.QuestionnaireResponse
3629
import org.hl7.fhir.r4.model.StringType
37-
import org.hl7.fhir.r4.model.TimeType
38-
import org.hl7.fhir.r4.model.Type
39-
import org.hl7.fhir.r4.model.UriType
4030
import org.smartregister.fhircore.engine.configuration.LinkIdType
4131
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
4232
import org.smartregister.fhircore.engine.configuration.UniqueIdAssignmentConfig
@@ -45,6 +35,7 @@ import org.smartregister.fhircore.engine.domain.model.ActionParameterType
4535
import org.smartregister.fhircore.engine.domain.model.RuleConfig
4636
import org.smartregister.fhircore.engine.domain.model.isEditable
4737
import org.smartregister.fhircore.engine.domain.model.isReadOnly
38+
import org.smartregister.fhircore.engine.util.castToType
4839

4940
fun QuestionnaireResponse.QuestionnaireResponseItemComponent.asLabel() =
5041
if (this.linkId != null) {
@@ -301,20 +292,3 @@ suspend fun Questionnaire.prepopulateUniqueIdAssignment(
301292
}
302293
}
303294
}
304-
305-
/** Cast string value (including json string) to the FHIR {@link org.hl7.fhir.r4.model.Type} */
306-
fun String.castToType(type: DataType): Type? {
307-
return when (type) {
308-
DataType.BOOLEAN -> BooleanType(this)
309-
DataType.DECIMAL -> DecimalType(this)
310-
DataType.INTEGER -> IntegerType(this)
311-
DataType.DATE -> DateType(this)
312-
DataType.DATETIME -> DateTimeType(this)
313-
DataType.TIME -> TimeType(this)
314-
DataType.STRING -> StringType(this)
315-
DataType.URI -> UriType(this)
316-
DataType.CODING -> this.tryDecodeJson<Coding>()
317-
DataType.QUANTITY -> this.tryDecodeJson<Quantity>()
318-
else -> null // TODO cast the (several) remaining Enumeration.DataTypes
319-
}
320-
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="16dp"
3+
android:height="16dp"
4+
android:viewportWidth="16"
5+
android:viewportHeight="16">
6+
<path
7+
android:pathData="M3.005,2H13.005L7.995,8.3L3.005,2ZM0.255,1.61C2.275,4.2 6.005,9 6.005,9V15C6.005,15.55 6.455,16 7.005,16H9.005C9.555,16 10.005,15.55 10.005,15V9C10.005,9 13.725,4.2 15.745,1.61C16.255,0.95 15.785,0 14.955,0H1.045C0.215,0 -0.255,0.95 0.255,1.61Z"
8+
android:fillColor="#ffffff"/>
9+
</vector>

0 commit comments

Comments
 (0)