Skip to content

Commit

Permalink
Decouple multi select data sync and filter (#3568)
Browse files Browse the repository at this point in the history
* Update top screen menu icons to render multiple

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Decouple syncing and syncing data on multi select view

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Support mutually exclusive select for multi-select widget

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Document class property

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Notify the user of locations missing coordinates

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Improve use experience by notifying the user of locations missing coordinates

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Remove clearing of map on destroy

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Refactor how data is passed between geowidget launcher and the maps fragment

Request the data from the map fragment instead of relying on the launcher to
publish the data. Previously the launcher could publish data before the subscriber
is ready thus leading to data loss.

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Extract constants

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Separate sync and data filter location ids state

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Show no locations selected dialog

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Show no location dialog on missing sync location ids

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* ⬆️ Update kujaku dependencies

* 🎨 Fix spotless error

* Fix refreshing data on map

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Run spotlessApply

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Delete unused method

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Apply default image size

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* 🎨 Update the icon arrangement

* Fix failing tests

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Run spotlessApply

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Fix icon positioning

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

---------

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>
Co-authored-by: Benjamin Mwalimu <dubdabasoduba@gmail.com>
  • Loading branch information
ellykits and dubdabasoduba authored Oct 30, 2024
1 parent 64a55e6 commit a6a62f1
Show file tree
Hide file tree
Showing 35 changed files with 747 additions and 522 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import org.smartregister.fhircore.engine.configuration.app.ApplicationConfigurat
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource
import org.smartregister.fhircore.engine.di.NetworkModule
import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
Expand All @@ -72,7 +73,7 @@ import org.smartregister.fhircore.engine.util.extension.generateMissingId
import org.smartregister.fhircore.engine.util.extension.interpolate
import org.smartregister.fhircore.engine.util.extension.referenceValue
import org.smartregister.fhircore.engine.util.extension.retrieveCompositionSections
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationIds
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState
import org.smartregister.fhircore.engine.util.extension.searchCompositionByIdentifier
import org.smartregister.fhircore.engine.util.extension.updateLastUpdated
import org.smartregister.fhircore.engine.util.helper.LocalizationHelper
Expand Down Expand Up @@ -743,7 +744,10 @@ constructor(
configService.defineResourceTags().find { it.type == ResourceType.Organization.name }
val mandatoryTags = configService.provideResourceTags(sharedPreferencesHelper)

val locationIds = context.retrieveRelatedEntitySyncLocationIds()
val locationIds =
context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.SYNC_DATA).map {
it.locationId
}

syncConfig.parameter
.map { it.resource as SearchParameter }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ data class ImageProperties(
val tint: String? = null,
val text: String? = null,
val imageConfig: ImageConfig? = null,
val size: Int? = null,
val size: Int? = 22,
val shape: ImageShape? = null,
val textColor: String? = null,
val actions: List<ActionConfig> = emptyList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFi
import org.smartregister.fhircore.engine.domain.model.Code
import org.smartregister.fhircore.engine.domain.model.DataQuery
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig
import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction
import org.smartregister.fhircore.engine.domain.model.RelatedResourceCount
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.domain.model.ResourceConfig
Expand All @@ -93,7 +94,7 @@ import org.smartregister.fhircore.engine.util.extension.filterBy
import org.smartregister.fhircore.engine.util.extension.filterByResourceTypeId
import org.smartregister.fhircore.engine.util.extension.generateMissingId
import org.smartregister.fhircore.engine.util.extension.loadResource
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationIds
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState
import org.smartregister.fhircore.engine.util.extension.updateFrom
import org.smartregister.fhircore.engine.util.extension.updateLastUpdated
import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor
Expand Down Expand Up @@ -934,7 +935,11 @@ constructor(
configComputedRuleValues: Map<String, Any>,
) =
if (filterByRelatedEntityLocation) {
val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds()
val syncLocationIds =
context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.FILTER_DATA).map {
it.locationId
}

val locationIds =
syncLocationIds
.map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } }
Expand Down Expand Up @@ -1017,7 +1022,10 @@ constructor(
val configComputedRuleValues = configRules.configRulesComputedValues()

if (filterByRelatedEntityLocationMetaTag) {
val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds()
val syncLocationIds =
context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.FILTER_DATA).map {
it.locationId
}
val locationIds =
syncLocationIds
.map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,9 @@ import timber.log.Timber

private const val PRACTITIONER_DETAILS_DATASTORE_JSON = "practitioner_details.json"
private const val USER_INFO_DATASTORE_JSON = "user_info.json"

private const val LOCATION_COORDINATES_DATASTORE_JSON = "location_coordinates.json"

private const val SYNC_LOCATION_IDS = "sync_location_ids.json"
private const val DATA_FILTER_LOCATION_IDS = "data_filter_location_ids.json"

val Context.practitionerProtoStore: DataStore<PractitionerDetails> by
dataStore(
Expand All @@ -64,6 +63,11 @@ val Context.syncLocationIdsProtoStore: DataStore<Map<String, SyncLocationState>>
fileName = SYNC_LOCATION_IDS,
serializer = SyncLocationIdDataStoreSerializer,
)
val Context.dataFilterLocationIdsProtoStore: DataStore<Map<String, SyncLocationState>> by
dataStore(
fileName = DATA_FILTER_LOCATION_IDS,
serializer = SyncLocationIdDataStoreSerializer,
)

@Singleton
class ProtoDataStore @Inject constructor(@ApplicationContext val context: Context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import kotlinx.serialization.Serializable
* @property rootNodeFhirPathExpression A key value pair containing a FHIRPath expression for
* extracting the value used to identify if the current resource is Root. The key is the FHIRPath
* expression while value is the content to compare against.
* @property viewActions The actions to be performed when the multiselect action button is pressed
* @property mutuallyExclusive Setup the multi choice checkbox such that only a single (root level)
* selection can be performed at a time.
*/
@Serializable
@Parcelize
Expand All @@ -37,4 +40,11 @@ data class MultiSelectViewConfig(
val parentIdFhirPathExpression: String,
val contentFhirPathExpression: String,
val rootNodeFhirPathExpression: KeyValueConfig,
val viewActions: List<MultiSelectViewAction> = listOf(MultiSelectViewAction.FILTER_DATA),
val mutuallyExclusive: Boolean = true,
) : java.io.Serializable, Parcelable

enum class MultiSelectViewAction {
SYNC_DATA,
FILTER_DATA,
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ fun <T> ColumnScope.MultiSelectView(
rootTreeNode: TreeNode<T>,
syncLocationStateMap: MutableMap<String, SyncLocationState>,
depth: Int = 0,
onChecked: () -> Unit,
content: @Composable (TreeNode<T>) -> Unit,
) {
val collapsedState = remember { mutableStateOf(false) }
Expand All @@ -55,6 +56,7 @@ fun <T> ColumnScope.MultiSelectView(
depth = depth,
content = content,
collapsedState = collapsedState,
onChecked = onChecked,
)
if (collapsedState.value) {
rootTreeNode.children.forEach {
Expand All @@ -63,18 +65,20 @@ fun <T> ColumnScope.MultiSelectView(
syncLocationStateMap = syncLocationStateMap,
depth = depth + 16,
content = content,
onChecked = onChecked,
)
}
}
}

@Composable
fun <T> MultiSelectCheckbox(
private fun <T> MultiSelectCheckbox(
syncLocationStateMap: MutableMap<String, SyncLocationState>,
currentTreeNode: TreeNode<T>,
depth: Int,
content: @Composable (TreeNode<T>) -> Unit,
collapsedState: MutableState<Boolean>,
onChecked: () -> Unit,
) {
val checked = remember { mutableStateOf(false) }
Column {
Expand Down Expand Up @@ -134,19 +138,12 @@ fun <T> MultiSelectCheckbox(
parent = parent.parent
}

// Select all the nested checkboxes
val treeNodeArrayDeque = ArrayDeque(currentTreeNode.children)

while (treeNodeArrayDeque.isNotEmpty()) {
val currentNode = treeNodeArrayDeque.removeFirst()
syncLocationStateMap[currentNode.id] =
SyncLocationState(
currentNode.id,
currentNode.parent?.id,
ToggleableState(checked.value),
)
currentNode.children.forEach { treeNodeArrayDeque.addLast(it) }
}
updateNestedCheckboxState(
currentTreeNode = currentTreeNode,
syncLocationStateMap = syncLocationStateMap,
checked = checked.value,
)
onChecked()
},
modifier = Modifier.padding(0.dp),
colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.primary),
Expand All @@ -156,3 +153,26 @@ fun <T> MultiSelectCheckbox(
}
}
}

/**
* This function selects/deselects all the children for the [currentTreeNode] based on the value for
* the [checked] parameter. The states for the [MultiSelectCheckbox] is updated in the
* [syncLocationStateMap].
*/
fun <T> updateNestedCheckboxState(
currentTreeNode: TreeNode<T>,
syncLocationStateMap: MutableMap<String, SyncLocationState>,
checked: Boolean,
) {
val treeNodeArrayDeque = ArrayDeque(currentTreeNode.children)
while (treeNodeArrayDeque.isNotEmpty()) {
val currentNode = treeNodeArrayDeque.removeFirst()
syncLocationStateMap[currentNode.id] =
SyncLocationState(
locationId = currentNode.id,
parentLocationId = currentNode.parent?.id,
toggleableState = ToggleableState(checked),
)
currentNode.children.forEach { treeNodeArrayDeque.addLast(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import java.io.Serializable
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import org.smartregister.fhircore.engine.datastore.dataFilterLocationIdsProtoStore
import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore
import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction
import org.smartregister.fhircore.engine.domain.model.SyncLocationState
import org.smartregister.fhircore.engine.ui.theme.DangerColor
import org.smartregister.fhircore.engine.ui.theme.DefaultColor
import org.smartregister.fhircore.engine.ui.theme.InfoColor
Expand Down Expand Up @@ -221,13 +226,25 @@ inline fun <reified T : Parcelable> Intent.parcelableArrayList(key: String): Arr
else -> @Suppress("DEPRECATION") getParcelableArrayListExtra(key)
}

suspend fun Context.retrieveRelatedEntitySyncLocationIds(): List<String> {
val selectedLocationStateMap = this.syncLocationIdsProtoStore.data.firstOrNull()
return selectedLocationStateMap
?.values
?.filter {
suspend fun Context.retrieveRelatedEntitySyncLocationState(
multiSelectViewAction: MultiSelectViewAction,
filterToggleableStateOn: Boolean = true,
): List<SyncLocationState> {
val selectedLocationStateMap =
withContext(Dispatchers.IO) {
val context = this@retrieveRelatedEntitySyncLocationState
when (multiSelectViewAction) {
MultiSelectViewAction.SYNC_DATA -> context.syncLocationIdsProtoStore.data.firstOrNull()
MultiSelectViewAction.FILTER_DATA ->
context.dataFilterLocationIdsProtoStore.data.firstOrNull()
}
}
return if (filterToggleableStateOn) {
selectedLocationStateMap?.values?.filter {
it.toggleableState == ToggleableState.On &&
selectedLocationStateMap[it.parentLocationId]?.toggleableState != ToggleableState.On
}
?.map { it.locationId } ?: emptyList()
} else {
selectedLocationStateMap?.values?.toList()
} ?: emptyList()
}
9 changes: 9 additions & 0 deletions android/engine/src/main/res/drawable/ic_filter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
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"
android:fillColor="#ffffff"/>
</vector>
1 change: 1 addition & 0 deletions android/engine/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,5 @@
<string name="retry">RETRY</string>
<string name="unsynced_data_present">There\'s some un-synced data</string>
<string name="missing_supervisor_contact">Supervisor contact missing or the provided phone number is invalid</string>
<string name="apply_filter">APPLY FILTER</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.geowidget.screens

import org.smartregister.fhircore.geowidget.model.GeoJsonFeature

interface GeoJsonDataRequester {
fun requestData(onReceiveData: (List<GeoJsonFeature>) -> Unit)
}
Loading

0 comments on commit a6a62f1

Please sign in to comment.