From e802df41dd5e764d1a5d436c83abb47ec2d1b046 Mon Sep 17 00:00:00 2001 From: hue Date: Wed, 14 Aug 2024 20:18:16 -0400 Subject: [PATCH] Update request redeploy querying all incidents --- .../CrisisCleanupTutorialViewTracker.kt | 5 +- .../com/crisiscleanup/ui/CrisisCleanupApp.kt | 1 - .../com/crisiscleanup/ui/TutorialGraphics.kt | 54 +++---- .../core/common/TutorialDirector.kt | 4 +- .../data/repository/IncidentsRepository.kt | 1 + .../OfflineFirstIncidentsRepository.kt | 18 +++ .../component/ListOptionsDropdown.kt | 1 + .../designsystem/icon/CrisisCleanupIcons.kt | 2 + .../core/designsystem/theme/Color.kt | 1 + .../core/model/data/CrisisCleanupList.kt | 2 +- .../crisiscleanup/core/model/data/Incident.kt | 10 ++ .../network/CrisisCleanupNetworkDataSource.kt | 7 + .../core/network/model/NetworkIncident.kt | 17 +++ .../core/network/retrofit/DataApiClient.kt | 21 +++ .../core/ui/LayoutSizePosition.kt | 2 +- .../core/ui/TutorialViewTracker.kt | 2 +- .../caseeditor/QueryIncidentsManager.kt | 8 +- .../feature/menu/MenuTutorialDirector.kt | 2 +- .../feature/menu/di/MenuModule.kt | 4 +- .../RequestRedeployViewModel.kt | 40 +++--- .../ui/IncidentsDropdown.kt | 57 ++------ .../ui/InviteTeammateScreen.kt | 74 ++++++---- .../ui/RequestRedeployScreen.kt | 132 +++++++++++++----- 23 files changed, 299 insertions(+), 166 deletions(-) diff --git a/app/src/main/java/com/crisiscleanup/CrisisCleanupTutorialViewTracker.kt b/app/src/main/java/com/crisiscleanup/CrisisCleanupTutorialViewTracker.kt index 041e00e71..980a650b0 100644 --- a/app/src/main/java/com/crisiscleanup/CrisisCleanupTutorialViewTracker.kt +++ b/app/src/main/java/com/crisiscleanup/CrisisCleanupTutorialViewTracker.kt @@ -11,12 +11,11 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class CrisisCleanupTutorialViewTracker @Inject constructor( -) : TutorialViewTracker { +class CrisisCleanupTutorialViewTracker @Inject constructor() : TutorialViewTracker { override val viewSizePositionLookup = SnapshotStateMap().also { it[AppNavBar] = LayoutSizePosition() it[IncidentSelectDropdown] = LayoutSizePosition() it[AccountToggle] = LayoutSizePosition() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index 77e34af27..6a4d72779 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -457,4 +457,3 @@ private fun ExpiredAccountAlert( } } } - diff --git a/app/src/main/java/com/crisiscleanup/ui/TutorialGraphics.kt b/app/src/main/java/com/crisiscleanup/ui/TutorialGraphics.kt index e4f1116bb..9a3561b67 100644 --- a/app/src/main/java/com/crisiscleanup/ui/TutorialGraphics.kt +++ b/app/src/main/java/com/crisiscleanup/ui/TutorialGraphics.kt @@ -174,11 +174,13 @@ private fun DrawScope.spotlightStepForwardOffset( viewSizePosition: LayoutSizePosition, ): Offset { val center = viewSizePosition.position.y + viewSizePosition.size.height * 0.5f - val y = size.height * (if (center > size.height * 0.5f) { - 0.2f - } else { - 0.8f - }) + val y = size.height * ( + if (center > size.height * 0.5f) { + 0.2f + } else { + 0.8f + } + ) val x = viewSizePosition.position.x + if (isHorizontalBar) { 32f } else { @@ -229,22 +231,24 @@ private fun DrawScope.menuTutorialDynamicContent( val lineX = size.width * 0.5f val lineStartY = - instructionOffset.y + (if (isSpotlightCenterAbove) { - -16f - } else { - val instructionConstraints = Constraints( - maxWidth = (size.width - instructionOffset.x).toInt(), - ) - val textLayout = textMeasurer.measure( - stepInstruction, - instructionStyle, - overflow = TextOverflow.Visible, - constraints = instructionConstraints, - ) - val textSize = textLayout.size + instructionOffset.y + ( + if (isSpotlightCenterAbove) { + -16f + } else { + val instructionConstraints = Constraints( + maxWidth = (size.width - instructionOffset.x).toInt(), + ) + val textLayout = textMeasurer.measure( + stepInstruction, + instructionStyle, + overflow = TextOverflow.Visible, + constraints = instructionConstraints, + ) + val textSize = textLayout.size - textSize.height.toFloat() + 16f - }) + textSize.height.toFloat() + 16f + } + ) val lineStart = Offset(lineX, lineStartY) val lineEndY = sizeOffset.topLeft.y + (if (isSpotlightCenterAbove) sizeOffset.size.height + 32f else -32f) @@ -275,11 +279,11 @@ private fun DrawScope.spotlightAboveStepForwardOffset( Offset(if (isHorizontalBar) 0f else 32f, 0f), ) val x = referencePosition.position.x + - if (isHorizontalBar) { - 32f - } else { - referencePosition.size.width * 0.2f - } + if (isHorizontalBar) { + 32f + } else { + referencePosition.size.width * 0.2f + } val y = size.height * 0.6f return Offset(x, y) } diff --git a/core/common/src/main/java/com/crisiscleanup/core/common/TutorialDirector.kt b/core/common/src/main/java/com/crisiscleanup/core/common/TutorialDirector.kt index 6523de45f..f5d449f5d 100644 --- a/core/common/src/main/java/com/crisiscleanup/core/common/TutorialDirector.kt +++ b/core/common/src/main/java/com/crisiscleanup/core/common/TutorialDirector.kt @@ -24,7 +24,7 @@ enum class TutorialStep { AccountInfo, ProvideAppFeedback, IncidentSelect, - End + End, } @Qualifier @@ -32,5 +32,5 @@ enum class TutorialStep { annotation class Tutorials(val director: CrisisCleanupTutorialDirectors) enum class CrisisCleanupTutorialDirectors { - Menu + Menu, } diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentsRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentsRepository.kt index bedd2b9c1..ed8f70ce1 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentsRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentsRepository.kt @@ -18,6 +18,7 @@ interface IncidentsRepository { suspend fun getIncident(id: Long, loadFormFields: Boolean = false): Incident? suspend fun getIncidents(startAt: Instant): List + suspend fun getIncidentsList(): List fun streamIncident(id: Long): Flow diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstIncidentsRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstIncidentsRepository.kt index 3204556fd..c81bd4bfd 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstIncidentsRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstIncidentsRepository.kt @@ -20,6 +20,7 @@ import com.crisiscleanup.core.database.model.asExternalModel import com.crisiscleanup.core.datastore.LocalAppPreferencesDataSource import com.crisiscleanup.core.model.data.INCIDENT_ORGANIZATIONS_STABLE_MODEL_BUILD_VERSION import com.crisiscleanup.core.model.data.Incident +import com.crisiscleanup.core.model.data.IncidentIdNameType import com.crisiscleanup.core.network.CrisisCleanupNetworkDataSource import com.crisiscleanup.core.network.model.NetworkIncident import com.crisiscleanup.core.network.model.NetworkIncidentLocation @@ -83,6 +84,23 @@ class OfflineFirstIncidentsRepository @Inject constructor( .map(PopulatedIncident::asExternalModel) } + override suspend fun getIncidentsList(): List { + try { + return networkDataSource.getIncidentsList() + .map { + IncidentIdNameType( + it.id, + it.name, + it.shortName, + disasterLiteral = it.type, + ) + } + } catch (e: Exception) { + logger.logException(e) + } + return emptyList() + } + override fun streamIncident(id: Long) = incidentDao.streamFormFieldsIncident(id).mapLatest { it?.asExternalModel() } diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/ListOptionsDropdown.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/ListOptionsDropdown.kt index 23d6e9f61..a521e3996 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/ListOptionsDropdown.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/ListOptionsDropdown.kt @@ -32,6 +32,7 @@ fun ListOptionsDropdown( Row( modifier .actionHeight() + // TODO Common dimensions .roundedOutline(radius = 3.dp) .clickable( onClick = onToggleDropdown, diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt index b7dc7b331..0beeddcd1 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt @@ -39,6 +39,7 @@ import androidx.compose.material.icons.filled.QuestionMark import androidx.compose.material.icons.filled.Remove import androidx.compose.material.icons.filled.Rotate90DegreesCcw import androidx.compose.material.icons.filled.Rotate90DegreesCw +import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.SentimentNeutral import androidx.compose.material.icons.filled.UnfoldMore import androidx.compose.material.icons.filled.Visibility @@ -88,6 +89,7 @@ object CrisisCleanupIcons { val MyLocation = icons.MyLocation val Organization = icons.Domain val Person = icons.Person + val PendingRequestRedeploy = icons.Schedule val Phone = icons.Phone val PhotoGrid = icons.PhotoLibrary val Play = icons.PlayArrow diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Color.kt index 73ec43d6a..719b5252b 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Color.kt @@ -37,6 +37,7 @@ val primaryBlueOneTenthColor = primaryBlueColor.copy(alpha = 0.1f) val primaryRedColor = Color(0xFFED4747) val primaryOrangeColor = Color(0xFFF79820) val devActionColor = Color(0xFFF50057) +val green600 = Color(0xFF43A047) internal val crisisCleanupYellow100 = Color(0xFFFFDC68) internal val crisisCleanupYellow100HalfTransparent = crisisCleanupYellow100.copy(alpha = 0.5f) val survivorNoteColor = crisisCleanupYellow100HalfTransparent diff --git a/core/model/src/main/java/com/crisiscleanup/core/model/data/CrisisCleanupList.kt b/core/model/src/main/java/com/crisiscleanup/core/model/data/CrisisCleanupList.kt index 78ed02855..8989e8606 100644 --- a/core/model/src/main/java/com/crisiscleanup/core/model/data/CrisisCleanupList.kt +++ b/core/model/src/main/java/com/crisiscleanup/core/model/data/CrisisCleanupList.kt @@ -33,7 +33,7 @@ val EmptyList = CrisisCleanupList( shared = ListShare.Private, permission = ListPermission.Read, incidentId = EmptyIncident.id, - incident = IncidentIdNameType(id = EmptyIncident.id, "", "", ""), + incident = EmptyIncidentIdNameType, ) enum class ListModel(val literal: String) { diff --git a/core/model/src/main/java/com/crisiscleanup/core/model/data/Incident.kt b/core/model/src/main/java/com/crisiscleanup/core/model/data/Incident.kt index fa4cd5250..91779f6e2 100644 --- a/core/model/src/main/java/com/crisiscleanup/core/model/data/Incident.kt +++ b/core/model/src/main/java/com/crisiscleanup/core/model/data/Incident.kt @@ -78,3 +78,13 @@ data class IncidentIdNameType( val disasterLiteral: String, val disaster: Disaster = disasterFromLiteral(disasterLiteral), ) + +val Incident.idNameType: IncidentIdNameType + get() = IncidentIdNameType( + id, + name, + shortName, + disasterLiteral, + ) + +val EmptyIncidentIdNameType = EmptyIncident.idNameType diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt b/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt index f768a78ef..fbd0fb684 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt @@ -14,6 +14,7 @@ import com.crisiscleanup.core.network.model.NetworkLocation import com.crisiscleanup.core.network.model.NetworkOrganizationShort import com.crisiscleanup.core.network.model.NetworkOrganizationsResult import com.crisiscleanup.core.network.model.NetworkPersonContact +import com.crisiscleanup.core.network.model.NetworkShortIncident import com.crisiscleanup.core.network.model.NetworkTeamResult import com.crisiscleanup.core.network.model.NetworkUserProfile import com.crisiscleanup.core.network.model.NetworkWorkTypeRequest @@ -39,6 +40,12 @@ interface CrisisCleanupNetworkDataSource { after: Instant? = null, ): List + suspend fun getIncidentsList( + fields: List = listOf("id", "name", "short_name", "incident_type"), + limit: Int = 250, + ordering: String = "-start_at", + ): List + suspend fun getIncidentLocations( locationIds: List, ): List diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkIncident.kt b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkIncident.kt index 2b0fb305c..c3dd0aee4 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkIncident.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkIncident.kt @@ -99,3 +99,20 @@ data class NetworkIncidentFormField( val name: String, ) } + +@Serializable +data class NetworkIncidentsListResult( + val errors: List? = null, + val count: Int? = null, + val results: List? = null, +) + +@Serializable +data class NetworkShortIncident( + val id: Long, + val name: String, + @SerialName("short_name") + val shortName: String, + @SerialName("incident_type") + val type: String, +) diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/retrofit/DataApiClient.kt b/core/network/src/main/java/com/crisiscleanup/core/network/retrofit/DataApiClient.kt index 1f8dce9b5..052baaeed 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/retrofit/DataApiClient.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/retrofit/DataApiClient.kt @@ -7,6 +7,7 @@ import com.crisiscleanup.core.network.model.NetworkCountResult import com.crisiscleanup.core.network.model.NetworkFlagsFormData import com.crisiscleanup.core.network.model.NetworkFlagsFormDataResult import com.crisiscleanup.core.network.model.NetworkIncidentResult +import com.crisiscleanup.core.network.model.NetworkIncidentsListResult import com.crisiscleanup.core.network.model.NetworkIncidentsResult import com.crisiscleanup.core.network.model.NetworkLanguageTranslationResult import com.crisiscleanup.core.network.model.NetworkLanguagesResult @@ -76,6 +77,16 @@ private interface DataSourceApi { after: Instant?, ): NetworkIncidentsResult + @GET("incidents_list") + suspend fun getIncidentsList( + @Query("fields") + fields: String, + @Query("limit") + limit: Int, + @Query("sort") + ordering: String, + ): NetworkIncidentsListResult + @TokenAuthenticationHeader @GET("locations") suspend fun getLocations( @@ -330,6 +341,16 @@ class DataApiClient @Inject constructor( it.results ?: emptyList() } + override suspend fun getIncidentsList( + fields: List, + limit: Int, + ordering: String, + ) = networkApi.getIncidentsList(fields.joinToString(","), limit, ordering) + .let { + it.errors?.tryThrowException() + it.results ?: emptyList() + } + override suspend fun getIncidentLocations(locationIds: List) = networkApi.getLocations(locationIds.joinToString(","), locationIds.size) .let { diff --git a/core/ui/src/main/java/com/crisiscleanup/core/ui/LayoutSizePosition.kt b/core/ui/src/main/java/com/crisiscleanup/core/ui/LayoutSizePosition.kt index d748a09d4..e9e66f066 100644 --- a/core/ui/src/main/java/com/crisiscleanup/core/ui/LayoutSizePosition.kt +++ b/core/ui/src/main/java/com/crisiscleanup/core/ui/LayoutSizePosition.kt @@ -11,4 +11,4 @@ data class LayoutSizePosition( ) val LayoutCoordinates.sizePosition: LayoutSizePosition - get() = LayoutSizePosition(size, positionInRoot()) \ No newline at end of file + get() = LayoutSizePosition(size, positionInRoot()) diff --git a/core/ui/src/main/java/com/crisiscleanup/core/ui/TutorialViewTracker.kt b/core/ui/src/main/java/com/crisiscleanup/core/ui/TutorialViewTracker.kt index 5713d5ac9..ca40144b8 100644 --- a/core/ui/src/main/java/com/crisiscleanup/core/ui/TutorialViewTracker.kt +++ b/core/ui/src/main/java/com/crisiscleanup/core/ui/TutorialViewTracker.kt @@ -5,4 +5,4 @@ import com.crisiscleanup.core.model.data.TutorialViewId interface TutorialViewTracker { val viewSizePositionLookup: SnapshotStateMap -} \ No newline at end of file +} diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/QueryIncidentsManager.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/QueryIncidentsManager.kt index 2e884504c..918229a02 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/QueryIncidentsManager.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/QueryIncidentsManager.kt @@ -5,6 +5,7 @@ import com.crisiscleanup.core.common.throttleLatest import com.crisiscleanup.core.data.repository.IncidentsRepository import com.crisiscleanup.core.model.data.Incident import com.crisiscleanup.core.model.data.IncidentIdNameType +import com.crisiscleanup.core.model.data.idNameType import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -42,12 +43,9 @@ class QueryIncidentsManager( private set private val allIncidentsShort = allIncidents.mapLatest { - val all = it.map { incident -> - with(incident) { - IncidentIdNameType(id, name, shortName, disasterLiteral) - } - } + val all = it.map(Incident::idNameType) + // TODO Redesign and separate state isLoadingAll.value = false all diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuTutorialDirector.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuTutorialDirector.kt index 838ea8311..fc8be931c 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuTutorialDirector.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuTutorialDirector.kt @@ -34,4 +34,4 @@ class MenuTutorialDirector @Inject constructor() : TutorialDirector { tutorialStep.value = nextStep return nextStep != TutorialStep.End } -} \ No newline at end of file +} diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/di/MenuModule.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/di/MenuModule.kt index 17bce71c1..87ac7cd5b 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/di/MenuModule.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/di/MenuModule.kt @@ -12,11 +12,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -interface DataModule { +interface MenuModule { @Singleton @Binds @Tutorials(CrisisCleanupTutorialDirectors.Menu) fun bindsTutorialDirector( director: MenuTutorialDirector, ): TutorialDirector -} \ No newline at end of file +} diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/RequestRedeployViewModel.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/RequestRedeployViewModel.kt index 65d8f0ce0..144bf6922 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/RequestRedeployViewModel.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/RequestRedeployViewModel.kt @@ -15,13 +15,14 @@ import com.crisiscleanup.core.data.repository.AccountDataRefresher import com.crisiscleanup.core.data.repository.AccountDataRepository import com.crisiscleanup.core.data.repository.IncidentsRepository import com.crisiscleanup.core.data.repository.RequestRedeployRepository -import com.crisiscleanup.core.model.data.EmptyIncident -import com.crisiscleanup.core.model.data.Incident +import com.crisiscleanup.core.model.data.EmptyIncidentIdNameType +import com.crisiscleanup.core.model.data.IncidentIdNameType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -29,7 +30,7 @@ import javax.inject.Inject @HiltViewModel class RequestRedeployViewModel @Inject constructor( - incidentsRepository: IncidentsRepository, + private val incidentsRepository: IncidentsRepository, accountDataRepository: AccountDataRepository, accountDataRefresher: AccountDataRefresher, private val requestRedeployRepository: RequestRedeployRepository, @@ -37,21 +38,18 @@ class RequestRedeployViewModel @Inject constructor( @Logger(CrisisCleanupLoggers.Account) private val logger: AppLogger, @Dispatcher(CrisisCleanupDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { - var requestedIncidentIds by mutableStateOf(emptySet()) - private set - + private val requestedIncidentsStream = MutableStateFlow>(emptySet()) + private val incidentsStream = MutableStateFlow?>(null) val viewState = combine( - incidentsRepository.incidents, + incidentsStream, accountDataRepository.accountData, - ::Pair, + requestedIncidentsStream, + ::Triple, ) - .mapLatest { (incidents, accountData) -> - val approvedIncidents = accountData.approvedIncidents - val incidentOptions = incidents - .filter { !approvedIncidents.contains(it.id) } - .toList() - .sortedByDescending(Incident::id) - RequestRedeployViewState.Ready(incidentOptions) + .filter { (incidents, _, _) -> incidents != null } + .mapLatest { (incidents, accountData, requestedIds) -> + val approvedIds = accountData.approvedIncidents + RequestRedeployViewState.Ready(incidents!!, approvedIds, requestedIds) } .stateIn( scope = viewModelScope, @@ -76,12 +74,14 @@ class RequestRedeployViewModel @Inject constructor( viewModelScope.launch(ioDispatcher) { accountDataRefresher.updateApprovedIncidents(true) - requestedIncidentIds = requestRedeployRepository.getRequestedIncidents() + requestedIncidentsStream.value = requestRedeployRepository.getRequestedIncidents() + + incidentsStream.value = incidentsRepository.getIncidentsList() } } - fun requestRedeploy(incident: Incident) { - if (incident == EmptyIncident) { + fun requestRedeploy(incident: IncidentIdNameType) { + if (incident == EmptyIncidentIdNameType) { return } @@ -116,6 +116,8 @@ class RequestRedeployViewModel @Inject constructor( sealed interface RequestRedeployViewState { data object Loading : RequestRedeployViewState data class Ready( - val incidents: List, + val incidents: List, + val approvedIncidentIds: Set, + val requestedIncidentIds: Set, ) : RequestRedeployViewState } diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/IncidentsDropdown.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/IncidentsDropdown.kt index 53d2ff87c..89ac0dbd7 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/IncidentsDropdown.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/IncidentsDropdown.kt @@ -2,62 +2,29 @@ package com.crisiscleanup.feature.organizationmanage.ui import androidx.compose.foundation.layout.width import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.platform.LocalDensity -import com.crisiscleanup.core.designsystem.theme.LocalFontStyles import com.crisiscleanup.core.designsystem.theme.listItemDropdownMenuOffset -import com.crisiscleanup.core.designsystem.theme.optionItemHeight -import com.crisiscleanup.core.model.data.Incident @Composable internal fun IncidentsDropdown( - incidents: List, contentSize: Size, showDropdown: Boolean, - onSelect: (Incident) -> Unit, onHideDropdown: () -> Unit, - isEditable: (Incident) -> Boolean = { true }, + optionsContent: @Composable () -> Unit, ) { - if (incidents.isNotEmpty()) { - DropdownMenu( - modifier = Modifier.width( - with(LocalDensity.current) { - contentSize.width.toDp().minus(listItemDropdownMenuOffset.x.times(2)) - }, - ), - expanded = showDropdown, - onDismissRequest = onHideDropdown, - offset = listItemDropdownMenuOffset, - ) { - IncidentOptions(incidents, onSelect, isEditable) - } - } -} - -@Composable -private fun IncidentOptions( - incidents: List, - onSelect: (Incident) -> Unit, - isEditable: (Incident) -> Boolean = { true }, -) { - for (incident in incidents) { - key(incident.id) { - DropdownMenuItem( - text = { - Text( - incident.name, - style = LocalFontStyles.current.header4, - ) - }, - onClick = { onSelect(incident) }, - modifier = Modifier.optionItemHeight(), - enabled = isEditable(incident), - ) - } + DropdownMenu( + modifier = Modifier.width( + with(LocalDensity.current) { + contentSize.width.toDp().minus(listItemDropdownMenuOffset.x.times(2)) + }, + ), + expanded = showDropdown, + onDismissRequest = onHideDropdown, + offset = listItemDropdownMenuOffset, + ) { + optionsContent() } } diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt index c67aceb90..cbffd5428 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt @@ -547,32 +547,58 @@ private fun NewOrganizationInput( val selectedIncident = incidentLookup[viewModel.selectedIncidentId] ?: EmptyIncident val selectIncidentHint = t("actions.select_incident") val incidents by viewModel.incidents.collectAsStateWithLifecycle() - Box( - Modifier - .listItemBottomPadding() - .fillMaxWidth(), - ) { - var showDropdown by remember { mutableStateOf(false) } - val onSelect = remember(viewModel) { - { incident: Incident -> - viewModel.selectedIncidentId = incident.id - showDropdown = false + if (incidents.isNotEmpty()) { + Box( + Modifier + .listItemBottomPadding() + .fillMaxWidth(), + ) { + var showDropdown by remember { mutableStateOf(false) } + val onSelect = remember(viewModel) { + { incident: Incident -> + viewModel.selectedIncidentId = incident.id + showDropdown = false + } + } + val onHideDropdown = remember(viewModel) { { showDropdown = false } } + ListOptionsDropdown( + text = selectedIncident.name.ifBlank { selectIncidentHint }, + isEditable = isEditable, + onToggleDropdown = { showDropdown = !showDropdown }, + modifier = Modifier.padding(16.dp), + dropdownIconContentDescription = selectIncidentHint, + ) { contentSize -> + IncidentsDropdown( + contentSize, + showDropdown, + onHideDropdown, + ) { + IncidentOptions( + incidents, + onSelect, + ) + } } } - val onHideDropdown = remember(viewModel) { { showDropdown = false } } - ListOptionsDropdown( - text = selectedIncident.name.ifBlank { selectIncidentHint }, - isEditable = isEditable, - onToggleDropdown = { showDropdown = !showDropdown }, - modifier = Modifier.padding(16.dp), - dropdownIconContentDescription = selectIncidentHint, - ) { contentSize -> - IncidentsDropdown( - incidents, - contentSize, - showDropdown, - onSelect, - onHideDropdown, + } +} + +@Composable +private fun IncidentOptions( + incidents: List, + onSelect: (Incident) -> Unit, +) { + for (incident in incidents) { + key(incident.id) { + DropdownMenuItem( + text = { + Text( + incident.name, + style = LocalFontStyles.current.header4, + ) + }, + onClick = { onSelect(incident) }, + modifier = Modifier.optionItemHeight(), ) } } diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/RequestRedeployScreen.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/RequestRedeployScreen.kt index c9fe2d93d..7511ec910 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/RequestRedeployScreen.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/RequestRedeployScreen.kt @@ -4,15 +4,20 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp @@ -24,10 +29,15 @@ import com.crisiscleanup.core.designsystem.component.BusyIndicatorFloatingTopCen import com.crisiscleanup.core.designsystem.component.ListOptionsDropdown import com.crisiscleanup.core.designsystem.component.TopAppBarBackAction import com.crisiscleanup.core.designsystem.component.cancelButtonColors +import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons +import com.crisiscleanup.core.designsystem.theme.LocalFontStyles +import com.crisiscleanup.core.designsystem.theme.green600 import com.crisiscleanup.core.designsystem.theme.listItemModifier import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy -import com.crisiscleanup.core.model.data.EmptyIncident -import com.crisiscleanup.core.model.data.Incident +import com.crisiscleanup.core.designsystem.theme.listItemSpacedByHalf +import com.crisiscleanup.core.designsystem.theme.optionItemHeight +import com.crisiscleanup.core.model.data.EmptyIncidentIdNameType +import com.crisiscleanup.core.model.data.IncidentIdNameType import com.crisiscleanup.feature.organizationmanage.RequestRedeployViewModel import com.crisiscleanup.feature.organizationmanage.RequestRedeployViewState @@ -49,7 +59,7 @@ fun RequestRedeployRoute( ) if (isLoading) { - Box { + Box(Modifier.fillMaxSize()) { BusyIndicatorFloatingTopCenter(true) } } else if (isRedeployRequested) { @@ -69,11 +79,12 @@ fun RequestRedeployRoute( val isTransient by viewModel.isTransient.collectAsStateWithLifecycle(true) val isEditable = !isTransient val errorMessage = viewModel.redeployErrorMessage - val requestedIncidentIds = viewModel.requestedIncidentIds + val requestedIncidentIds = readyState.requestedIncidentIds + val approvedIncidentIds = readyState.approvedIncidentIds val isRequestingRedeploy by viewModel.isRequestingRedeploy.collectAsStateWithLifecycle() - var selectedIncident by remember { mutableStateOf(EmptyIncident) } + var selectedIncident by remember { mutableStateOf(EmptyIncidentIdNameType) } val onSelectIncident = remember(viewModel) { - { incident: Incident -> + { incident: IncidentIdNameType -> selectedIncident = incident } } @@ -83,6 +94,7 @@ fun RequestRedeployRoute( isEditable, incidents, requestedIncidentIds, + approvedIncidentIds, errorMessage, selectedIncident.name.ifBlank { selectIncidentHint }, selectIncidentHint, @@ -111,7 +123,7 @@ fun RequestRedeployRoute( .testTag("requestRedeploySubmitAction") .weight(1f), text = t("actions.submit"), - enabled = isEditable && selectedIncident != EmptyIncident, + enabled = isEditable && selectedIncident != EmptyIncidentIdNameType, indicateBusy = isRequestingRedeploy, onClick = { viewModel.requestRedeploy(selectedIncident) }, ) @@ -125,22 +137,17 @@ fun RequestRedeployRoute( @Composable private fun RequestRedeployContent( isEditable: Boolean, - incidents: List, + incidents: List, requestedIncidentIds: Set, + approvedIncidentIds: Set, errorMessage: String, selectedIncidentText: String, selectIncidentHint: String, - setSelectedIncident: (Incident) -> Unit, + setSelectedIncident: (IncidentIdNameType) -> Unit, rememberKey: Any, ) { val t = LocalAppTranslator.current - val isIncidentEditable = remember(requestedIncidentIds) { - { incident: Incident -> - !requestedIncidentIds.contains(incident.id) - } - } - Text( t("requestRedeploy.choose_an_incident"), listItemModifier, @@ -154,28 +161,81 @@ private fun RequestRedeployContent( ) } - var showDropdown by remember { mutableStateOf(false) } - val onSelectIncident = remember(rememberKey) { - { incident: Incident -> - setSelectedIncident(incident) - showDropdown = false + if (incidents.isNotEmpty()) { + var showDropdown by remember { mutableStateOf(false) } + val onSelectIncident = remember(rememberKey) { + { incident: IncidentIdNameType -> + setSelectedIncident(incident) + showDropdown = false + } + } + val onHideDropdown = remember(rememberKey) { { showDropdown = false } } + ListOptionsDropdown( + text = selectedIncidentText, + isEditable = isEditable, + onToggleDropdown = { showDropdown = !showDropdown }, + modifier = Modifier.padding(16.dp), + dropdownIconContentDescription = selectIncidentHint, + ) { contentSize -> + IncidentsDropdown( + contentSize, + showDropdown, + onHideDropdown, + ) { + IncidentOptions( + incidents, + approvedIncidentIds, + requestedIncidentIds, + onSelectIncident, + ) + } } } - val onHideDropdown = remember(rememberKey) { { showDropdown = false } } - ListOptionsDropdown( - text = selectedIncidentText, - isEditable = isEditable, - onToggleDropdown = { showDropdown = !showDropdown }, - modifier = Modifier.padding(16.dp), - dropdownIconContentDescription = selectIncidentHint, - ) { contentSize -> - IncidentsDropdown( - incidents, - contentSize, - showDropdown, - onSelectIncident, - onHideDropdown, - isEditable = isIncidentEditable, - ) +} + +@Composable +private fun IncidentOptions( + incidents: List, + approvedIds: Set, + requestedIds: Set, + onSelect: (IncidentIdNameType) -> Unit, +) { + val t = LocalAppTranslator.current + for (incident in incidents) { + key(incident.id) { + val isApproved = approvedIds.contains(incident.id) + val isRequested = requestedIds.contains(incident.id) + DropdownMenuItem( + text = { + Row( + horizontalArrangement = listItemSpacedByHalf, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + incident.name, + Modifier.weight(1f), + style = LocalFontStyles.current.header4, + ) + if (isApproved) { + Icon( + CrisisCleanupIcons.Check, + contentDescription = t("~~{incident_name} is already approved") + .replace("{incident_name}", incident.shortName), + tint = green600, + ) + } else if (isRequested) { + Icon( + CrisisCleanupIcons.PendingRequestRedeploy, + contentDescription = t("~~{incident_name} is already awaiting redeploy") + .replace("{incident_name}", incident.shortName), + ) + } + } + }, + onClick = { onSelect(incident) }, + modifier = Modifier.optionItemHeight(), + enabled = !(isApproved || isRequested), + ) + } } }