From 5e86439f49f67e5c3b799869f070946bd878b594 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 21 Oct 2024 13:04:56 -0700 Subject: [PATCH 01/51] SpeziOnboarding --- .../onboarding/OnboardingNavigationEvent.kt | 1 - .../onboarding/consent/ConsentDocument.kt | 21 ++++++++++ .../consent/ConsentDocumentExport.kt | 10 +++++ .../onboarding/consent/ConsentViewState.kt | 13 ++++++ .../onboarding/consent/ExportConfiguration.kt | 39 ++++++++++++++++++ ...reen.kt => OnboardingConsentComposable.kt} | 9 +++- .../module/onboarding/consent/ViewState.kt | 7 ++++ .../onboarding/OnboardingComposableBuilder.kt | 41 +++++++++++++++++++ .../onboarding/onboarding/OnboardingStack.kt | 4 ++ 9 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/{ConsentScreen.kt => OnboardingConsentComposable.kt} (89%) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt index 084bb6644..cef78a7f1 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt @@ -3,7 +3,6 @@ package edu.stanford.spezi.module.onboarding import edu.stanford.spezi.core.navigation.NavigationEvent sealed class OnboardingNavigationEvent : NavigationEvent { - data object InvitationCodeScreen : OnboardingNavigationEvent() data class OnboardingScreen(val clearBackStack: Boolean) : OnboardingNavigationEvent() data object SequentialOnboardingScreen : OnboardingNavigationEvent() diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt new file mode 100644 index 000000000..c907d3973 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt @@ -0,0 +1,21 @@ +package edu.stanford.spezi.module.onboarding.consent + +import androidx.compose.runtime.MutableState +import edu.stanford.spezi.core.design.component.StringResource + +data class ConsentDocument( + val markdown: suspend () -> ByteArray, + val viewState: MutableState, + val givenNameTitle: StringResource = LocalizationDefaults.givenNameTitle, + val givenNamePlaceholder: StringResource = LocalizationDefaults.givenNamePlaceholder, + val familyNameTitle: StringResource = LocalizationDefaults.familyNameTitle, + val familyNamePlaceholder: StringResource = LocalizationDefaults.familyNamePlaceholder, + val exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), +) { + object LocalizationDefaults { + val givenNameTitle = StringResource("Given Name") + val givenNamePlaceholder = StringResource("Given Name Placeholder") + val familyNameTitle = StringResource("Family Name") + val familyNamePlaceholder = StringResource("Family Name Placeholder") + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt new file mode 100644 index 000000000..1d60d6c43 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt @@ -0,0 +1,10 @@ +package edu.stanford.spezi.module.onboarding.consent + +import android.graphics.pdf.PdfDocument + +class ConsentDocumentExport( + private val documentIdentifier: String, + private val document: suspend () -> PdfDocument +) { + suspend fun createDocument() = document() +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt new file mode 100644 index 000000000..79998c259 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt @@ -0,0 +1,13 @@ +package edu.stanford.spezi.module.onboarding.consent + +import android.graphics.pdf.PdfDocument + +sealed interface ConsentViewState { + data class Base(val viewState: ViewState) : ConsentViewState + data object NamesEntered : ConsentViewState + data object Signing : ConsentViewState + data object Signed : ConsentViewState + data object Export : ConsentViewState + data class Exported(val document: PdfDocument) : ConsentViewState + data object Storing : ConsentViewState +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt new file mode 100644 index 000000000..a15eef4b6 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt @@ -0,0 +1,39 @@ +package edu.stanford.spezi.module.onboarding.consent + +import edu.stanford.spezi.core.design.component.StringResource + +data class ConsentDocumentExportConfiguration( + val paperSize: PaperSize = PaperSize.usLetter, + val consentTitle: StringResource = LocalizationDefaults.exportedConsentFormTitle, + val includingTimestamp: Boolean = true, +) { + object LocalizationDefaults { + val exportedConsentFormTitle = StringResource("Consent") + } + + data class PaperSize( + val width: Double, + val height: Double, + ) { + companion object { + private const val A4_WIDTH_IN_INCHES = 8.3 + private const val A4_HEIGHT_IN_INCHES = 11.7 + + private const val US_LETTER_WIDTH_IN_INCHES = 8.5 + private const val US_LETTER_HEIGHT_IN_INCHES = 11.0 + + val usLetter get() = usLetter() + val dinA4 get() = dinA4() + + fun dinA4(pointsPerInch: Double = 72.0) = PaperSize( + width = A4_WIDTH_IN_INCHES * pointsPerInch, + height = A4_HEIGHT_IN_INCHES * pointsPerInch + ) + + fun usLetter(pointsPerInch: Double = 72.0) = PaperSize( + width = US_LETTER_WIDTH_IN_INCHES * pointsPerInch, + height = US_LETTER_HEIGHT_IN_INCHES * pointsPerInch + ) + } + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt similarity index 89% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt index f72326e1e..e13974727 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel +import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.theme.Spacings @@ -22,7 +23,13 @@ import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.utils.extensions.testIdentifier @Composable -fun ConsentScreen() { +fun OnboardingConsentComposable( + markdown: suspend () -> ByteArray, + action: suspend () -> Unit, + title: StringResource? = StringResource("Consent"), + identifier: String = "ConsentDocument", + exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), +) { val viewModel: ConsentViewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsState() diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt new file mode 100644 index 000000000..2253fc5ca --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt @@ -0,0 +1,7 @@ +package edu.stanford.spezi.module.onboarding.consent + +sealed interface ViewState { + data object Idle : ViewState + data object Processing : ViewState + data class Error(val throwable: Throwable?) : ViewState +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt new file mode 100644 index 000000000..c32b6e9de --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt @@ -0,0 +1,41 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +data class OnboardingStep( + val identifier: String, + val composable: @Composable () -> Unit +) + +data class OnboardingComposableBuilder( + var list: MutableList +) { + fun step(id: String, composable: @Composable () -> Unit) { + list.add(OnboardingStep(id, composable)) + } +} + +fun buildOnboardingSteps( + build: OnboardingComposableBuilder.() -> Unit +): List { + val builder = OnboardingComposableBuilder(mutableListOf()) + build(builder) + return builder.list +} + +fun test() { + buildOnboardingSteps { + step("") { + Text("") + } + + val bool = true + + if (bool) { + step("check") { + Text("check") + } + } + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt new file mode 100644 index 000000000..4d1d6a98a --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt @@ -0,0 +1,4 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +class OnboardingStack { +} \ No newline at end of file From bfc3a6faf9058c2b3d75b6c8d7a359ae32214f14 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Thu, 24 Oct 2024 10:06:13 -0700 Subject: [PATCH 02/51] update --- .../onboarding/EngageConsentManager.kt | 1 - .../engagehf/onboarding/OnboardingModule.kt | 1 - .../component/markdown/MarkdownComposable.kt | 10 ++ .../component/markdown/MarkdownParser.kt | 1 - .../component/markdown/MarkdownUiState.kt | 5 + .../component/markdown/MarkdownViewModel.kt | 29 ++++ .../utils/foundation}/PersonNameComponents.kt | 2 +- .../spezi/modules/contact/ContactFactory.kt | 2 +- .../simulator/ContactComposableSimulator.kt | 2 +- .../modules/contact/ContactComposable.kt | 2 +- .../spezi/modules/contact/model/Contact.kt | 1 + .../onboarding/di/TestOnboardingModule.kt | 1 - .../onboarding/consent/ConsentConstraint.kt | 7 + .../onboarding/consent/ConsentDataSource.kt | 22 +++ .../onboarding/consent/ConsentDocument.kt | 153 +++++++++++++++++- .../consent/ConsentDocumentExport.kt | 8 +- .../onboarding/consent/ConsentManager.kt | 11 -- ...reationService.kt => ConsentPdfService.kt} | 36 +++-- .../onboarding/consent/ConsentUiState.kt | 15 +- .../onboarding/consent/ConsentViewModel.kt | 47 +++--- .../onboarding/consent/ConsentViewState.kt | 1 + .../onboarding/consent/ExportConfiguration.kt | 6 +- .../consent/OnboardingConsentComposable.kt | 99 +++++++++--- .../module/onboarding/consent/SignaturePad.kt | 138 ---------------- .../module/onboarding/consent/ViewState.kt | 7 - .../onboarding/OnboardingActions.kt | 48 ++++++ .../onboarding/OnboardingComposable.kt | 43 +++++ .../onboarding/onboarding/OnboardingStack.kt | 3 - .../onboarding/onboarding/OnboardingTitle.kt | 37 +++++ .../flow/IllegalOnboardingStepComposable.kt | 10 ++ .../spezi/module/onboarding/views/Standard.kt | 3 + .../module/onboarding/views/SuspendButton.kt | 61 +++++++ .../module/onboarding/views/ViewState.kt | 17 ++ .../module/onboarding/views/ViewStateAlert.kt | 33 ++++ 34 files changed, 613 insertions(+), 249 deletions(-) create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt rename {modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model => core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/foundation}/PersonNameComponents.kt (91%) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/{PdfCreationService.kt => ConsentPdfService.kt} (79%) delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt index 784f71d44..9e2233a07 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt @@ -3,7 +3,6 @@ package edu.stanford.bdh.engagehf.onboarding import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent import edu.stanford.spezi.core.navigation.Navigator import edu.stanford.spezi.core.utils.MessageNotifier -import edu.stanford.spezi.module.onboarding.consent.ConsentManager import javax.inject.Inject class EngageConsentManager @Inject internal constructor( diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt index a59c013e6..73edc50ff 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt @@ -4,7 +4,6 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import edu.stanford.spezi.module.onboarding.consent.ConsentManager import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeRepository import edu.stanford.spezi.module.onboarding.onboarding.OnboardingRepository import edu.stanford.spezi.module.onboarding.sequential.SequentialOnboardingRepository diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt index a01503e04..be2d1ae6d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -14,6 +15,15 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles +@Composable +fun MarkdownComposable(data: suspend () -> ByteArray) { + // TODO: Figure out why hiltViewModel is not working and how one would do that anyways + val viewModel = remember { MarkdownViewModel(data, MarkdownParser()) } + val uiState = viewModel.uiState.collectAsState() + + MarkdownComponent(uiState.value.elements ?: emptyList()) +} + @Composable fun MarkdownComponent(markdownElements: List) { LazyColumn(modifier = Modifier.padding(Spacings.medium)) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt index f5673f699..e28cbf74a 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt @@ -7,7 +7,6 @@ private const val HEADING_LEVEL_2 = 2 private const val HEADING_LEVEL_3 = 3 class MarkdownParser @Inject constructor() { - fun parse(text: String): List = buildList { text.lines().forEach { line -> when { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt new file mode 100644 index 000000000..87ed6e178 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt @@ -0,0 +1,5 @@ +package edu.stanford.spezi.core.design.component.markdown + +data class MarkdownUiState( + val elements: List? = null +) \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt new file mode 100644 index 000000000..97f2cc80e --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt @@ -0,0 +1,29 @@ +package edu.stanford.spezi.core.design.component.markdown + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.nio.charset.StandardCharsets + +internal class MarkdownViewModel @AssistedInject internal constructor( + @Assisted private val data: suspend () -> ByteArray, + private val markdownParser: MarkdownParser, +) : ViewModel() { + private val _uiState = MutableStateFlow(MarkdownUiState()) + val uiState: StateFlow = _uiState + + init { + viewModelScope.launch { + val markdownText = data().toString(StandardCharsets.UTF_8) + val markdownElements = markdownParser.parse(markdownText) + _uiState.update { + it.copy(elements = markdownElements) + } + } + } +} diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/foundation/PersonNameComponents.kt similarity index 91% rename from modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt rename to core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/foundation/PersonNameComponents.kt index d2987a340..6cc517be3 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt +++ b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/foundation/PersonNameComponents.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.modules.contact.model +package edu.stanford.spezi.core.utils.foundation data class PersonNameComponents( val namePrefix: String? = null, diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt index a0c2259ab..95e2accee 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt @@ -4,9 +4,9 @@ import android.location.Address import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.call import edu.stanford.spezi.modules.contact.model.email import edu.stanford.spezi.modules.contact.model.text diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt index 3aee4bdf1..bb68474c1 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt @@ -10,9 +10,9 @@ import androidx.compose.ui.test.onChildAt import androidx.test.platform.app.InstrumentationRegistry import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.testing.onNodeWithIdentifier +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import edu.stanford.spezi.modules.contact.ContactComposableTestIdentifier import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.formatted class ContactComposableSimulator( diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt index 2c2fbeff4..a6bcd1cb5 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt @@ -33,11 +33,11 @@ import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.utils.extensions.testIdentifier +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import edu.stanford.spezi.modules.contact.component.AddressCard import edu.stanford.spezi.modules.contact.component.ContactOptionCard import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.call import edu.stanford.spezi.modules.contact.model.email import edu.stanford.spezi.modules.contact.model.text diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt index cef15dfeb..181d8faa9 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt @@ -3,6 +3,7 @@ package edu.stanford.spezi.modules.contact.model import android.location.Address import androidx.compose.ui.graphics.vector.ImageVector import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import java.util.UUID /** diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt index 96d631f97..1c7bf87de 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt @@ -7,7 +7,6 @@ import dagger.hilt.testing.TestInstallIn import edu.stanford.spezi.module.account.di.AccountModule import edu.stanford.spezi.module.account.manager.InvitationAuthManager import edu.stanford.spezi.module.account.manager.UserSessionManager -import edu.stanford.spezi.module.onboarding.consent.ConsentManager import edu.stanford.spezi.module.onboarding.fakes.FakeOnboardingRepository import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeRepository import edu.stanford.spezi.module.onboarding.onboarding.OnboardingRepository diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt new file mode 100644 index 000000000..c388f1347 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt @@ -0,0 +1,7 @@ +package edu.stanford.spezi.module.onboarding.consent + +import edu.stanford.spezi.module.onboarding.views.Standard + +interface ConsentConstraint : Standard { + suspend fun store(consent: ConsentDocumentExport) +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt new file mode 100644 index 000000000..f1837b081 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt @@ -0,0 +1,22 @@ +package edu.stanford.spezi.module.onboarding.consent + +import android.graphics.pdf.PdfDocument +import edu.stanford.spezi.module.onboarding.views.Standard +import javax.inject.Inject + +class ConsentDataSource { + @Inject lateinit var standard: Standard + + init { + if (standard !is ConsentConstraint) { + TODO("on iOS: fatalError") + } + } + + suspend fun store(document: suspend () -> PdfDocument, identifier: String) { + (standard as? ConsentConstraint)?.let { consentConstraint -> + val export = ConsentDocumentExport(identifier, document) + consentConstraint.store(export) + } ?: TODO("on iOS: fatalError") + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt index c907d3973..71d2e45a6 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt @@ -1,16 +1,43 @@ package edu.stanford.spezi.module.onboarding.consent +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.Spacings +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents +import edu.stanford.spezi.module.onboarding.views.ViewState +import java.nio.charset.StandardCharsets data class ConsentDocument( - val markdown: suspend () -> ByteArray, - val viewState: MutableState, - val givenNameTitle: StringResource = LocalizationDefaults.givenNameTitle, - val givenNamePlaceholder: StringResource = LocalizationDefaults.givenNamePlaceholder, - val familyNameTitle: StringResource = LocalizationDefaults.familyNameTitle, - val familyNamePlaceholder: StringResource = LocalizationDefaults.familyNamePlaceholder, - val exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), + private val markdown: suspend () -> ByteArray, + private val viewState: MutableState, + private val givenNameTitle: StringResource = LocalizationDefaults.givenNameTitle, + private val givenNamePlaceholder: StringResource = LocalizationDefaults.givenNamePlaceholder, + private val familyNameTitle: StringResource = LocalizationDefaults.familyNameTitle, + private val familyNamePlaceholder: StringResource = LocalizationDefaults.familyNamePlaceholder, + private val exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), ) { object LocalizationDefaults { val givenNameTitle = StringResource("Given Name") @@ -18,4 +45,114 @@ data class ConsentDocument( val familyNameTitle = StringResource("Family Name") val familyNamePlaceholder = StringResource("Family Name Placeholder") } -} \ No newline at end of file + + @OptIn(ExperimentalComposeUiApi::class) + @Composable + internal fun Composable( + modifier: Modifier = Modifier, + uiState: ConsentUiState, + onAction: (ConsentAction) -> Unit + ) { + val givenName = uiState.name.givenName ?: "" + val familyName = uiState.name.familyName ?: "" + + val keyboardController = LocalSoftwareKeyboardController.current + Column(modifier = modifier) { + OutlinedTextField( + value = givenName, + onValueChange = { + onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.FIRST_NAME)) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(givenNameTitle.text()) }, + singleLine = true, + placeholder = { Text(givenNamePlaceholder.text()) }, + trailingIcon = { Icon(Icons.Filled.Info, contentDescription = "Information Icon") } + ) + Spacer(modifier = Modifier.height(Spacings.small)) + OutlinedTextField( + value = familyName, + onValueChange = { + onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.LAST_NAME)) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(familyNameTitle.text()) }, + placeholder = { Text(familyNamePlaceholder.text()) }, + singleLine = true, + trailingIcon = { + Icon( + Icons.Filled.Info, + contentDescription = "Information Icon" + ) + } + ) + + + if (givenName.isNotBlank() && familyName.isNotBlank()) { + Spacer(modifier = Modifier.height(Spacings.medium)) + Text("Signature:") + SignatureCanvas( + paths = uiState.paths.toMutableList(), + firstName = givenName, + lastName = familyName, + onPathAdd = { path -> + onAction(ConsentAction.AddPath(path)) + keyboardController?.hide() + } + ) + Spacer(modifier = Modifier.height(Spacings.medium)) + Row(modifier = Modifier.fillMaxWidth()) { + FilledTonalButton( + onClick = { + if (uiState.paths.isNotEmpty()) { + onAction(ConsentAction.Undo) + } + }, + enabled = uiState.paths.isNotEmpty(), + modifier = Modifier.weight(1f), + ) { + Text("Undo") + } + Spacer(modifier = Modifier.width(Spacings.medium)) + } + } + } + } +} + +@SuppressLint("UnrememberedMutableState") +@Preview +@Composable +private fun ConsentDocumentComposablePreview( + @PreviewParameter(ConsentDocumentComposablePreviewProvider::class) data: ConsentDocumentComposablePreviewData, +) { + ConsentDocument( + markdown = { "".toByteArray(StandardCharsets.UTF_8) }, + viewState = remember { mutableStateOf(ConsentViewState.Base(ViewState.Idle)) }, + ).Composable( + uiState = ConsentUiState( + name = data.name, + paths = data.paths + ) + ) {} +} + +private data class ConsentDocumentComposablePreviewData( + val paths: MutableList, + val name: PersonNameComponents, +) + +private class ConsentDocumentComposablePreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ConsentDocumentComposablePreviewData( + paths = mutableListOf(Path()), + name = PersonNameComponents(givenName = "", familyName = "") + ), + @Suppress("MagicNumber") + ConsentDocumentComposablePreviewData( + paths = mutableListOf(Path().apply { lineTo(100f, 100f) }.apply { lineTo(250f, 200f) }), + name = PersonNameComponents(givenName = "Jane", familyName = "Doe") + + ) + ) +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt index 1d60d6c43..022d75d50 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt @@ -3,8 +3,12 @@ package edu.stanford.spezi.module.onboarding.consent import android.graphics.pdf.PdfDocument class ConsentDocumentExport( - private val documentIdentifier: String, + private val documentIdentifier: String = Defaults.DOCUMENT_IDENTIFIER, private val document: suspend () -> PdfDocument ) { + private object Defaults { + const val DOCUMENT_IDENTIFIER = "ConsentDocument" + } + suspend fun createDocument() = document() -} \ No newline at end of file +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt deleted file mode 100644 index 784a4ec24..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt +++ /dev/null @@ -1,11 +0,0 @@ -package edu.stanford.spezi.module.onboarding.consent - -/** - * A interface that needs to be implemented and provided by the app to provide the consent text and handle consent actions. - * @see edu.stanford.bdh.engagehf.onboarding.EngageConsentManager - */ -interface ConsentManager { - suspend fun getMarkdownText(): String - suspend fun onConsented() - suspend fun onConsentFailure(error: Throwable) -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/PdfCreationService.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt similarity index 79% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/PdfCreationService.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt index 4b6312de1..27cc7489e 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/PdfCreationService.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt @@ -8,29 +8,39 @@ import android.graphics.pdf.PdfDocument import android.text.Layout import android.text.StaticLayout import android.text.TextPaint +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asAndroidPath import edu.stanford.spezi.core.coroutines.di.Dispatching import edu.stanford.spezi.core.design.component.markdown.MarkdownElement +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import java.io.ByteArrayOutputStream import java.time.LocalDate import javax.inject.Inject -internal class PdfCreationService @Inject internal constructor( +internal class ConsentPdfService @Inject internal constructor( @Dispatching.IO private val ioCoroutineDispatcher: CoroutineDispatcher, ) { - suspend fun createPdf(uiState: ConsentUiState): ByteArray = withContext(ioCoroutineDispatcher) { + suspend fun createDocument( + configuration: ConsentDocumentExportConfiguration, + name: PersonNameComponents, + signaturePaths: List, + markdownElements: List, + ): PdfDocument = withContext(ioCoroutineDispatcher) { val pdfDocument = PdfDocument() - val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create() + val pageInfo = PdfDocument.PageInfo.Builder( + configuration.paperSize.width.toInt(), + configuration.paperSize.height.toInt(), + 1 + ).create() val page = pdfDocument.startPage(pageInfo) val canvas = page.canvas var yOffset = 50f - uiState.markdownElements.forEach { + markdownElements.forEach { yOffset = when (it) { is MarkdownElement.Heading -> drawHeading(canvas, it, yOffset) is MarkdownElement.Paragraph -> drawParagraph(canvas, it, yOffset) @@ -39,19 +49,17 @@ internal class PdfCreationService @Inject internal constructor( } } yOffset += 50f - yOffset = drawNamesAndSignature(canvas, uiState, yOffset) + yOffset = drawNameAndSignature(canvas, name, signaturePaths, yOffset) pdfDocument.finishPage(page) - val outputStream = ByteArrayOutputStream() - pdfDocument.writeTo(outputStream) - pdfDocument.close() - outputStream.toByteArray() + pdfDocument } - private fun drawNamesAndSignature( + private fun drawNameAndSignature( canvas: Canvas, - uiState: ConsentUiState, + name: PersonNameComponents, + signaturePaths: List, yOffset: Float, ): Float { val paintNames = Paint().apply { @@ -59,7 +67,7 @@ internal class PdfCreationService @Inject internal constructor( textSize = 14f } canvas.drawText( - "First Name: ${uiState.firstName.value} Last Name: ${uiState.lastName.value} Date: ${LocalDate.now()}", + "First Name: ${name.givenName ?: ""} Last Name: ${name.familyName ?: ""} Date: ${LocalDate.now()}", 10f, yOffset, paintNames @@ -74,7 +82,7 @@ internal class PdfCreationService @Inject internal constructor( canvas.save() canvas.scale(scaleFactor, scaleFactor) - uiState.paths.forEach { path -> + signaturePaths.forEach { path -> val androidPath = path.asAndroidPath() val offsetPath = android.graphics.Path(androidPath) offsetPath.offset(0f, yOffset * 5) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt index 25b60f35c..cf7a263bf 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt @@ -2,15 +2,17 @@ package edu.stanford.spezi.module.onboarding.consent import androidx.compose.ui.graphics.Path import edu.stanford.spezi.core.design.component.markdown.MarkdownElement +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents +import edu.stanford.spezi.module.onboarding.views.ViewState -data class ConsentUiState( - val firstName: FieldState = FieldState(value = "", error = false), - val lastName: FieldState = FieldState(value = "", error = false), +internal data class ConsentUiState( + val name: PersonNameComponents = PersonNameComponents(), val paths: List = emptyList(), val markdownElements: List = emptyList(), + val viewState: ConsentViewState = ConsentViewState.Base(ViewState.Idle), ) { val isValidForm: Boolean = - firstName.value.isNotBlank() && lastName.value.isNotBlank() && paths.isNotEmpty() + (name.givenName?.isNotBlank() ?: false) && (name.familyName?.isNotBlank() ?: false) && paths.isNotEmpty() } data class FieldState( @@ -26,5 +28,8 @@ sealed interface ConsentAction { data class TextFieldUpdate(val newValue: String, val type: TextFieldType) : ConsentAction data class AddPath(val path: Path) : ConsentAction data object Undo : ConsentAction - data object Consent : ConsentAction + data class Consent( + val documentIdentifier: String, + val exportConfiguration: ConsentDocumentExportConfiguration + ) : ConsentAction } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt index 2fbefdab2..ffd615606 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt @@ -3,8 +3,7 @@ package edu.stanford.spezi.module.onboarding.consent import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import edu.stanford.spezi.core.design.component.markdown.MarkdownParser -import edu.stanford.spezi.module.account.manager.UserSessionManager +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -13,35 +12,22 @@ import javax.inject.Inject @HiltViewModel internal class ConsentViewModel @Inject internal constructor( - private val consentManager: ConsentManager, - private val markdownParser: MarkdownParser, - private val pdfCreationService: PdfCreationService, - private val userSessionManager: UserSessionManager, + private val pdfService: ConsentPdfService, + private val consentDataSource: ConsentDataSource, ) : ViewModel() { private val _uiState = MutableStateFlow(ConsentUiState()) val uiState: StateFlow = _uiState - init { - viewModelScope.launch { - val markdownText = consentManager.getMarkdownText() - _uiState.update { - it.copy(markdownElements = markdownParser.parse(markdownText)) - } - } - } - fun onAction(action: ConsentAction) { when (action) { is ConsentAction.TextFieldUpdate -> { when (action.type) { TextFieldType.FIRST_NAME -> { - val firstName = FieldState(value = action.newValue) - _uiState.update { it.copy(firstName = firstName) } + _uiState.update { it.copy(name = it.name.copy(givenName = action.newValue)) } } TextFieldType.LAST_NAME -> { - val lastName = FieldState(value = action.newValue) - _uiState.update { it.copy(lastName = lastName) } + _uiState.update { it.copy(name = it.name.copy(familyName = action.newValue)) } } } } @@ -55,17 +41,20 @@ internal class ConsentViewModel @Inject internal constructor( } is ConsentAction.Consent -> { - onConsentAction() + viewModelScope.launch { + consentDataSource.store( + { + pdfService.createDocument( + action.exportConfiguration, + uiState.value.name, + uiState.value.paths, + uiState.value.markdownElements, + ) + }, + action.documentIdentifier, + ) + } } } } - - private fun onConsentAction() { - viewModelScope.launch { - val pdfBytes = pdfCreationService.createPdf(uiState = uiState.value) - userSessionManager.uploadConsentPdf(pdfBytes = pdfBytes) - .onSuccess { consentManager.onConsented() } - .onFailure { consentManager.onConsentFailure(error = it) } - } - } } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt index 79998c259..eda047335 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt @@ -1,6 +1,7 @@ package edu.stanford.spezi.module.onboarding.consent import android.graphics.pdf.PdfDocument +import edu.stanford.spezi.module.onboarding.views.ViewState sealed interface ConsentViewState { data class Base(val viewState: ViewState) : ConsentViewState diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt index a15eef4b6..8e21ac155 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt @@ -22,15 +22,17 @@ data class ConsentDocumentExportConfiguration( private const val US_LETTER_WIDTH_IN_INCHES = 8.5 private const val US_LETTER_HEIGHT_IN_INCHES = 11.0 + private const val DEFAULT_POINTS_PER_INCH = 72.0 + val usLetter get() = usLetter() val dinA4 get() = dinA4() - fun dinA4(pointsPerInch: Double = 72.0) = PaperSize( + fun dinA4(pointsPerInch: Double = DEFAULT_POINTS_PER_INCH) = PaperSize( width = A4_WIDTH_IN_INCHES * pointsPerInch, height = A4_HEIGHT_IN_INCHES * pointsPerInch ) - fun usLetter(pointsPerInch: Double = 72.0) = PaperSize( + fun usLetter(pointsPerInch: Double = DEFAULT_POINTS_PER_INCH) = PaperSize( width = US_LETTER_WIDTH_IN_INCHES * pointsPerInch, height = US_LETTER_HEIGHT_IN_INCHES * pointsPerInch ) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt index e13974727..781aa2983 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt @@ -4,62 +4,111 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Path import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel +import edu.stanford.spezi.core.design.component.Button import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent +import edu.stanford.spezi.core.design.component.markdown.MarkdownComposable import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.utils.extensions.testIdentifier +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents +import edu.stanford.spezi.module.onboarding.onboarding.OnboardingComposable +import edu.stanford.spezi.module.onboarding.onboarding.OnboardingTitle +import edu.stanford.spezi.module.onboarding.views.ViewState +import kotlinx.coroutines.launch @Composable fun OnboardingConsentComposable( markdown: suspend () -> ByteArray, action: suspend () -> Unit, - title: StringResource? = StringResource("Consent"), - identifier: String = "ConsentDocument", - exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), + title: StringResource? = remember { StringResource("Consent") }, + identifier: String = remember { "ConsentDocument" }, + exportConfiguration: ConsentDocumentExportConfiguration = remember { ConsentDocumentExportConfiguration() }, ) { val viewModel: ConsentViewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsState() - ConsentScreen( - onAction = viewModel::onAction, uiState = uiState + OnboardingConsentComposableContent( + markdown = markdown, + action = action, + title = title, + identifier = identifier, + exportConfiguration = exportConfiguration, + uiState = uiState, + onAction = viewModel::onAction ) } @Composable -private fun ConsentScreen( +internal fun OnboardingConsentComposableContent( + markdown: suspend () -> ByteArray, + action: suspend () -> Unit, + title: StringResource?, + identifier: String, + exportConfiguration: ConsentDocumentExportConfiguration, uiState: ConsentUiState, - onAction: (ConsentAction) -> Unit, + onAction: (ConsentAction) -> Unit ) { - Column( + val actionScope = rememberCoroutineScope() + OnboardingComposable( modifier = Modifier .testIdentifier(ConsentScreenTestIdentifier.ROOT) - .fillMaxSize() - .padding(Spacings.medium) - ) { + .fillMaxSize(), + title = { + title?.let { + OnboardingTitle(it) + } + }, + content = { + ConsentDocument( + markdown = markdown, + viewState = remember { mutableStateOf(ConsentViewState.Base(ViewState.Idle)) }, + exportConfiguration = exportConfiguration, + ).Composable( + modifier = Modifier.padding(bottom = Spacings.medium), + uiState = uiState, + onAction = onAction, + ) + }, + action = { + Button( + onClick = { + actionScope.launch { + onAction(ConsentAction.Consent(identifier, exportConfiguration)) + action() + } + }, + enabled = uiState.isValidForm, + modifier = Modifier.fillMaxWidth(1f) + ) { + Text("I Consent") + } + } + ) + Column { Spacer(modifier = Modifier.height(Spacings.medium)) - MarkdownComponent(markdownElements = uiState.markdownElements) + MarkdownComposable(markdown) Spacer( modifier = Modifier .height(Spacings.small) .weight(1f) ) - SignaturePad( - uiState = uiState, - onAction = onAction, - ) } } @@ -69,7 +118,15 @@ private fun ConsentScreenPreview( @PreviewParameter(ConsentScreenPreviewProvider::class) uiState: ConsentUiState, ) { SpeziTheme { - ConsentScreen(uiState = uiState, onAction = { }) + OnboardingConsentComposableContent( + markdown = { ByteArray(0) }, + action = {}, + title = null, + identifier = "ConsentDocument", + exportConfiguration = ConsentDocumentExportConfiguration(), + uiState = uiState, + onAction = {} + ) } } @@ -77,16 +134,14 @@ private fun ConsentScreenPreview( private class ConsentScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( ConsentUiState( - firstName = FieldState("John"), - lastName = FieldState("Doe"), + name = PersonNameComponents(givenName = "John", familyName = "Doe"), paths = mutableListOf(Path().apply { lineTo(100f, 100f) }), markdownElements = listOf( MarkdownElement.Heading(1, "Consent"), MarkdownElement.Paragraph("Please sign below to indicate your consent."), ), ), ConsentUiState( - firstName = FieldState(""), - lastName = FieldState(""), + name = PersonNameComponents(givenName = "", familyName = ""), paths = mutableListOf(), ) ) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt deleted file mode 100644 index 1b6e7e6b3..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt +++ /dev/null @@ -1,138 +0,0 @@ -@file:Suppress("MagicNumber") - -package edu.stanford.spezi.module.onboarding.consent - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info -import androidx.compose.material3.Button -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import edu.stanford.spezi.core.design.theme.Spacings - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -internal fun SignaturePad( - uiState: ConsentUiState, - onAction: (ConsentAction) -> Unit, -) { - val keyboardController = LocalSoftwareKeyboardController.current - Column { - OutlinedTextField( - value = uiState.firstName.value, - onValueChange = { - onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.FIRST_NAME)) - }, - modifier = Modifier.fillMaxWidth(), - label = { Text("First Name") }, - singleLine = true, - isError = uiState.firstName.error, - trailingIcon = { Icon(Icons.Filled.Info, contentDescription = "Information Icon") } - ) - Spacer(modifier = Modifier.height(Spacings.small)) - OutlinedTextField( - value = uiState.lastName.value, - onValueChange = { - onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.LAST_NAME)) - }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Last Name") }, - isError = uiState.lastName.error, - singleLine = true, - trailingIcon = { - Icon( - Icons.Filled.Info, - contentDescription = "Information Icon" - ) - } - ) - - if (uiState.firstName.value.isNotBlank() && uiState.lastName.value.isNotBlank()) { - Spacer(modifier = Modifier.height(Spacings.medium)) - Text("Signature:") - SignatureCanvas( - paths = uiState.paths.toMutableList(), - firstName = uiState.firstName.value, - lastName = uiState.lastName.value, - onPathAdd = { path -> - onAction(ConsentAction.AddPath(path)) - keyboardController?.hide() - } - ) - Spacer(modifier = Modifier.height(Spacings.medium)) - Row(modifier = Modifier.fillMaxWidth()) { - FilledTonalButton( - onClick = { - if (uiState.paths.isNotEmpty()) { - onAction(ConsentAction.Undo) - } - }, - enabled = uiState.paths.isNotEmpty(), - modifier = Modifier.weight(1f), - ) { - Text("Undo") - } - Spacer(modifier = Modifier.width(Spacings.medium)) - Button( - onClick = { - onAction(ConsentAction.Consent) - }, - enabled = uiState.isValidForm, - modifier = Modifier.weight(1f) - ) { - Text("I Consent") - } - } - } - } -} - -@Preview -@Composable -private fun SignaturePadPreview( - @PreviewParameter(SignaturePadPreviewProvider::class) data: SignaturePadPreviewData, -) { - SignaturePad( - uiState = ConsentUiState( - firstName = FieldState(data.firstName), - lastName = FieldState(data.lastName), - paths = data.paths - ) - ) {} -} - -private data class SignaturePadPreviewData( - val paths: MutableList, - val firstName: String, - val lastName: String, -) - -private class SignaturePadPreviewProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - SignaturePadPreviewData( - paths = mutableListOf(Path()), - firstName = "", - lastName = "" - ), - SignaturePadPreviewData( - paths = mutableListOf(Path().apply { lineTo(100f, 100f) }.apply { lineTo(250f, 200f) }), - firstName = "Jane", - lastName = "Doe" - ) - ) -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt deleted file mode 100644 index 2253fc5ca..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package edu.stanford.spezi.module.onboarding.consent - -sealed interface ViewState { - data object Idle : ViewState - data object Processing : ViewState - data class Error(val throwable: Throwable?) : ViewState -} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt new file mode 100644 index 000000000..9279176eb --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt @@ -0,0 +1,48 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.module.onboarding.views.SuspendButton +import edu.stanford.spezi.module.onboarding.views.ViewState +import edu.stanford.spezi.module.onboarding.views.ViewStateAlert + +@Composable +fun OnboardingActions( + primaryText: StringResource, + primaryAction: suspend () -> Unit, + secondaryText: StringResource? = null, + secondaryAction: (suspend () -> Unit)? = null +) { + val primaryActionState = remember { mutableStateOf(ViewState.Idle) } + val secondaryActionState = remember { mutableStateOf(ViewState.Idle) } + + ViewStateAlert(primaryActionState) + ViewStateAlert(secondaryActionState) + + Column(Modifier.padding(top = 10.dp)) { + SuspendButton(primaryActionState, primaryAction) { + Text( + primaryText.text(), + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 38.dp) + ) + } + secondaryText?.let { secondaryText -> + secondaryAction?.let { secondaryAction -> + SuspendButton(secondaryActionState, secondaryAction) { + Text(secondaryText.text()) + } + } + } + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt new file mode 100644 index 000000000..f40a698d8 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt @@ -0,0 +1,43 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp + + +@Composable +fun OnboardingComposable( + modifier: Modifier = Modifier, + title: @Composable () -> Unit = {}, + content: @Composable () -> Unit, + action: (@Composable () -> Unit)? = null, +) { + val size = remember { mutableStateOf(IntSize.Zero) } + Box(modifier.onSizeChanged { size.value = it }) { + LazyColumn { + item { + Column(Modifier.heightIn(min = size.value.height.dp)) { + Column { + title() + content() + } + action?.let { action -> + Spacer(Modifier) + action() + } + Spacer(Modifier.height(10.dp)) + } + } + } + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt index 4d1d6a98a..4a79dd02d 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt @@ -1,4 +1 @@ package edu.stanford.spezi.module.onboarding.onboarding - -class OnboardingStack { -} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt new file mode 100644 index 000000000..4a775e512 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt @@ -0,0 +1,37 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import edu.stanford.spezi.core.design.component.StringResource + +@Composable +fun OnboardingTitle(title: StringResource, subtitle: StringResource? = null) { + OnboardingTitle(title.text(), subtitle?.text()) +} + +@Composable +fun OnboardingTitle(title: String, subtitle: String? = null) { + Column(Modifier.padding(vertical = 8.dp)) { + Text( + title, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + + subtitle?.let { subtitle -> + Text( + subtitle, + modifier = Modifier.padding(bottom = 8.dp), + textAlign = TextAlign.Center + ) + } + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt new file mode 100644 index 000000000..14f99b9f8 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt @@ -0,0 +1,10 @@ +package edu.stanford.spezi.module.onboarding.onboarding.flow + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import edu.stanford.spezi.core.design.component.StringResource + +@Composable +internal fun IllegalOnboardingStepComposable() { + Text(StringResource("Illegal onboarding step").text()) +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt new file mode 100644 index 000000000..bb196a0a2 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt @@ -0,0 +1,3 @@ +package edu.stanford.spezi.module.onboarding.views + +interface Standard diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt new file mode 100644 index 000000000..52bce60cb --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt @@ -0,0 +1,61 @@ +package edu.stanford.spezi.module.onboarding.views + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import edu.stanford.spezi.core.design.component.Button +import edu.stanford.spezi.core.utils.UUID +import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +private enum class SuspendButtonState { + IDLE, DISABLED, DISABLED_AND_PROCESSING; +} + +@Composable +fun SuspendButton( + state: MutableState, + action: suspend () -> Unit, + label: @Composable () -> Unit +) { + val buttonState = remember { mutableStateOf(SuspendButtonState.IDLE) } + val coroutineScope = rememberCoroutineScope() + val debounceScope = rememberCoroutineScope() + + DisposableEffect(remember { UUID() }) { + onDispose { + coroutineScope.cancel() + } + } + + Button( + onClick = { + if (state.value == ViewState.Processing) return@Button + buttonState.value = SuspendButtonState.DISABLED + + // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? + state.value = ViewState.Processing + + coroutineScope.launch { + runCatching { + action() + if (state.value != ViewState.Idle) { + // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? + state.value = ViewState.Idle + } + }.onFailure { + state.value = ViewState.Error(it) + } + + buttonState.value = SuspendButtonState.IDLE + } + }, + enabled = !coroutineScope.isActive + ) { + label() + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt new file mode 100644 index 000000000..787b05b31 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt @@ -0,0 +1,17 @@ +package edu.stanford.spezi.module.onboarding.views + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import edu.stanford.spezi.core.design.component.StringResource + +sealed interface ViewState { + data object Idle : ViewState + data object Processing : ViewState + data class Error(val throwable: Throwable?) : ViewState + + val errorTitle: String + @Composable @ReadOnlyComposable get() = StringResource("Error").text() + + val errorDescription: String + @Composable @ReadOnlyComposable get() = if (this is Error) throwable?.localizedMessage ?: "" else "" +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt new file mode 100644 index 000000000..df758c582 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt @@ -0,0 +1,33 @@ +package edu.stanford.spezi.module.onboarding.views + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState + +@Composable +fun ViewStateAlert(state: MutableState) { + if (state.value is ViewState.Error) { + AlertDialog( + title = { + Text(text = state.value.errorTitle) + }, + text = { + Text(text = state.value.errorDescription) + }, + onDismissRequest = { + state.value = ViewState.Idle + }, + confirmButton = { + TextButton( + onClick = { + state.value = ViewState.Idle + } + ) { + Text("Okay") + } + } + ) + } +} \ No newline at end of file From aaf29df4d649d061344b20593490e11a6ce9e4e6 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Fri, 8 Nov 2024 22:35:34 -0800 Subject: [PATCH 03/51] SpeziViews --- .../personalInfo/PersonNameComponents.kt | 37 ++++++ .../personalInfo/UserProfileComposable.kt | 62 ++++++++++ .../views/personalInfo/fields/NameFieldRow.kt | 79 +++++++++++++ .../personalInfo/fields/NameTextField.kt | 62 ++++++++++ .../validation/CascadingValidationEffect.kt | 5 + .../views/validation/ValidationEngine.kt | 109 ++++++++++++++++++ .../views/validation/ValidationModifier.kt | 70 +++++++++++ .../design/views/validation/ValidationRule.kt | 30 +++++ .../validation/ValidationRuleDefaults.kt | 46 ++++++++ .../ValidationDebounceDuration.kt | 8 ++ .../configuration/ValidationEngine.kt | 6 + .../ValidationEngineConfiguration.kt | 9 ++ .../state/CapturedValidationState.kt | 29 +++++ .../state/CapturedValidationStateEntries.kt | 13 +++ .../state/FailedValidationResult.kt | 15 +++ .../validation/state/ReceiveValidation.kt | 18 +++ .../validation/state/ValidationContext.kt | 43 +++++++ .../views/ValidationResultsComposable.kt | 4 + .../validation/views/VerifiableTextField.kt | 4 + .../ProcessingDebounceDuration.kt | 6 + .../views/views/layout/DescriptionGridRow.kt | 74 ++++++++++++ .../views/views/model/OperationState.kt | 5 + .../design/views/views/model/ViewState.kt | 17 +++ .../views/viewModifier/OnChangeListener.kt | 22 ++++ .../viewState/OperationStateAlert.kt | 16 +++ .../viewModifier/viewState/ViewStateAlert.kt | 34 ++++++ .../viewModifier/viewState/ViewStateMapper.kt | 14 +++ .../views/views/views/button/SuspendButton.kt | 62 ++++++++++ .../spezi/core/design/SpeziValidationTest.kt | 34 ++++++ .../contact/model/PersonNameComponents.kt | 22 ---- 30 files changed, 933 insertions(+), 22 deletions(-) create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/OnChangeListener.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt create mode 100644 core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt delete mode 100644 modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt new file mode 100644 index 000000000..471037d08 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt @@ -0,0 +1,37 @@ +package edu.stanford.spezi.core.design.views.personalInfo + +data class PersonNameComponents( + var namePrefix: String? = null, + var givenName: String? = null, + var middleName: String? = null, + var familyName: String? = null, + var nameSuffix: String? = null, + var nickname: String? = null, +) { + enum class FormatStyle { + ABBREVIATED, SHORT, MEDIUM, LONG + } + + fun formatted(style: FormatStyle = FormatStyle.LONG): String { + return when (style) { + FormatStyle.LONG -> listOfNotNull( + namePrefix, + givenName, + nickname?.let { "\"$it\"" }, + middleName, + familyName, + nameSuffix + ).joinToString(" ") + FormatStyle.MEDIUM -> + TODO("Not yet implemented.") + FormatStyle.SHORT -> + TODO("Not yet implemented.") + FormatStyle.ABBREVIATED -> listOfNotNull( + givenName, + middleName, + familyName, + ).joinToString("") + .filter { it.isUpperCase() } + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt new file mode 100644 index 000000000..9cf2a82c8 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt @@ -0,0 +1,62 @@ +package edu.stanford.spezi.core.design.views.personalInfo + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import edu.stanford.spezi.core.design.theme.Colors +import edu.stanford.spezi.core.design.theme.lighten +import kotlin.math.min + +@Composable +fun UserProfileComposable( + modifier: Modifier = Modifier, + name: PersonNameComponents, + imageLoader: suspend () -> ImageVector? = { null }, // TODO: Use ImageResource instead! +) { + val image = remember { mutableStateOf(null) } + val size = remember { mutableStateOf(IntSize.Zero) } + + LaunchedEffect(Unit) { + image.value = imageLoader() + } + + Box(modifier.onSizeChanged { size.value = it }.aspectRatio(1f)) { + val sideLength = min(size.value.height, size.value.width).dp + Box(modifier.size(sideLength, sideLength), contentAlignment = Alignment.Center) { + image.value?.let { + Image( + it, + null, + Modifier + .clip(CircleShape) + .background(Colors.background, CircleShape) + ) + } ?: run { + Box(Modifier.background(Colors.secondary, CircleShape).fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + name.formatted(PersonNameComponents.FormatStyle.ABBREVIATED), + fontSize = (sideLength.value * 0.2).sp, + color = Colors.secondary.lighten(), + ) + } + } + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt new file mode 100644 index 000000000..be99e55f0 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt @@ -0,0 +1,79 @@ +package edu.stanford.spezi.core.design.views.personalInfo.fields + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid +import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.views.layout.DescriptionGridRow +import kotlin.reflect.KMutableProperty1 + +@Composable +fun NameFieldRow( + description: StringResource, + name: MutableState, + component: KMutableProperty1, + label: @Composable () -> Unit, +) { + NameFieldRow( + name = name, + component = component, + description = { Text(description.text()) }, + label = label + ) +} + +@Composable +fun NameFieldRow( + name: MutableState, + component: KMutableProperty1, + description: @Composable () -> Unit, + label: @Composable () -> Unit, +) { + DescriptionGridRow( + description = description, + content = { + NameTextField(name, component) { + label() + } + } + ) +} + +@ThemePreviews +@Composable +private fun NameFieldRowPreview() { + val name = remember { mutableStateOf(PersonNameComponents()) } + + Column { + NameFieldRow( + name, + PersonNameComponents::givenName, + description = { Text("First") } + ) { + Text("enter first name") + } + + HorizontalDivider(Modifier.padding(vertical = 15.dp)) + + // Last Name Field + NameFieldRow( + name, + PersonNameComponents::familyName, + description = { Text("Last") } + ) { + Text("enter last name") + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt new file mode 100644 index 000000000..c60be6459 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt @@ -0,0 +1,62 @@ +package edu.stanford.spezi.core.design.views.personalInfo.fields + +import android.app.Person +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import kotlin.reflect.KMutableProperty1 + +@Composable +fun NameTextField( + label: StringResource, + name: MutableState, + component: KMutableProperty1, + prompt: StringResource? = null, +) { + NameTextField(name, component, prompt) { + Text(label.text()) + } +} + +@Composable +fun NameTextField( + name: MutableState, + component: KMutableProperty1, + prompt: StringResource? = null, + label: @Composable () -> Unit, +) { + // TODO: Figure out which other options to set on the keyboard for names + TextField( + component.get(name.value) ?: "", + onValueChange = { + if (it.isBlank()) { + component.set(name.value, null) + } else { + component.set(name.value, it) + } + }, + keyboardOptions = KeyboardOptions( + autoCorrect = false, + ), + // TODO: Check if placeholder is the right fit for the prompt property here. + placeholder = prompt?.let { { Text(it.text()) } }, + label = label + ) +} + +@ThemePreviews +@Composable +private fun NameTextFieldPreview() { + val name = remember { mutableStateOf(PersonNameComponents()) } + + NameTextField(name, PersonNameComponents::givenName) { + Text("Enter first name") + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt new file mode 100644 index 000000000..e1676ff23 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt @@ -0,0 +1,5 @@ +package edu.stanford.spezi.core.design.views.validation + +enum class CascadingValidationEffect { + CONTINUE, INTERCEPT +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt new file mode 100644 index 000000000..f20eb35c7 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt @@ -0,0 +1,109 @@ +package edu.stanford.spezi.core.design.views.validation + +import android.provider.Settings.Global +import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION +import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.EnumSet +import kotlin.time.Duration + +typealias ValidationEngineConfiguration = EnumSet + +class ValidationEngine( + val rules: List, + var debounceDuration: Duration = DEFAULT_VALIDATION_DEBOUNCE_DURATION, + var configuration: ValidationEngineConfiguration = ValidationEngineConfiguration.noneOf(ConfigurationOption::class.java), +) { + private enum class Source { + SUBMIT, MANUAL + } + + enum class ConfigurationOption { + HIDE_FAILED_VALIDATION_ON_EMPTY_SUBMIT, + CONSIDER_NO_INPUT_AS_VALID, + } + + var validationResults: List = emptyList() + private set + + private var computedInputValid: Boolean? = null + + val inputValid: Boolean get() = + computedInputValid ?: configuration.contains(ConfigurationOption.CONSIDER_NO_INPUT_AS_VALID) + + private var source: Source? = null + private var inputWasEmpty = true + + val isDisplayingValidationErrors: Boolean get() { + val gotResults = validationResults.isNotEmpty() + + if (configuration.contains(ConfigurationOption.HIDE_FAILED_VALIDATION_ON_EMPTY_SUBMIT)) { + return gotResults && (source == Source.MANUAL || !inputWasEmpty) + } + + return gotResults + } + + val displayedValidationResults: List get() = + if (isDisplayingValidationErrors) validationResults else emptyList() + + private var debounceJob: Job? = null + + @Suppress("detekt:LoopWithTooManyJumpStatements") + private fun computeFailedValidations(input: String): List { + val results = mutableListOf() + + for (rule in rules) { + val result = rule.validate(input) ?: break + results.add(result) + // TODO: Logging + if (rule.effect == CascadingValidationEffect.INTERCEPT) break + } + + return results + } + + private fun computeValidation(input: String, source: Source) { + this.source = source + this.inputWasEmpty = input.isEmpty() + + this.validationResults = computeFailedValidations(input) + this.computedInputValid = validationResults.isEmpty() + } + + fun submit(input: String, debounce: Boolean = false) { + if (!debounce || computedInputValid == false) { + computeValidation(input, Source.SUBMIT) + } else { + this.debounce { + this.computeValidation(input, Source.SUBMIT) + } + } + } + + fun runValidation(input: String) { + computeValidation(input, Source.MANUAL) + } + + @OptIn(DelicateCoroutinesApi::class) + private fun debounce(task: () -> Unit) { + debounceJob?.cancel() + // TODO: Think about whether to not use GlobalScope here + debounceJob = GlobalScope.launch { + delay(debounceDuration) + + if (!isActive) return@launch + + task() + debounceJob = null + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt new file mode 100644 index 000000000..560a39d90 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt @@ -0,0 +1,70 @@ +package edu.stanford.spezi.core.design.views.validation + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationDebounce +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngineConfiguration +import edu.stanford.spezi.core.design.views.validation.state.CapturedValidationState +import edu.stanford.spezi.core.design.views.validation.state.LocalCapturedValidationStateEntries +import kotlin.time.Duration + +@Composable +fun Validate( + predicate: Boolean, + message: StringResource, + content: @Composable () -> Unit, +) { + val rule = ValidationRule( + rule = { it.isEmpty() }, + message = message + ) + Validate( + input = if (predicate) "" else "FALSE", + rules = listOf(rule), + content = content + ) +} + +@SuppressLint("MutableCollectionMutableState") // TODO: Get rid of this +@Composable +fun Validate( + input: String, + rules: List, + content: @Composable () -> Unit, +) { + val previousInput = remember { mutableStateOf(input) } + val validationDebounce = LocalValidationDebounce.current + val previousValidationDebounce = remember { mutableStateOf(null) } + val validationEngineConfiguration = LocalValidationEngineConfiguration.current + val previousValidationEngineConfiguration = remember { mutableStateOf(null) } + val engine = remember { ValidationEngine(rules, validationDebounce, validationEngineConfiguration) } + + if (input != previousInput.value) { + engine.submit(input, debounce = true) + } + + if (validationDebounce != previousValidationDebounce.value) { + engine.debounceDuration = validationDebounce + previousValidationDebounce.value = validationDebounce + } + + if (validationEngineConfiguration != previousValidationEngineConfiguration.value) { + engine.configuration = validationEngineConfiguration + previousValidationEngineConfiguration.value = validationEngineConfiguration + } + + val hasFocus = remember { mutableStateOf(false) } + LocalCapturedValidationStateEntries.current + .add(CapturedValidationState(engine, input, hasFocus)) + + CompositionLocalProvider(LocalValidationEngine provides engine) { + content() + // TODO: onSubmit missing + // TODO: focused missing + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt new file mode 100644 index 000000000..93fbdd97c --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt @@ -0,0 +1,30 @@ +package edu.stanford.spezi.core.design.views.validation + +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult +import java.util.UUID + +data class ValidationRule internal constructor( + val id: UUID = UUID.randomUUID(), + val rule: (String) -> Boolean, + val message: StringResource, + val effect: CascadingValidationEffect = CascadingValidationEffect.CONTINUE +) { + companion object { + operator fun invoke(regex: Regex, message: StringResource): ValidationRule = + ValidationRule(rule = { regex.matchEntire(it) != null }, message = message) + operator fun invoke(copy: ValidationRule, message: StringResource): ValidationRule = + ValidationRule(rule = copy.rule, message = message) + } + + val intercepting: ValidationRule + get() = ValidationRule(id, rule, message, CascadingValidationEffect.INTERCEPT) + + override fun equals(other: Any?): Boolean = + id == (other as? ValidationRule)?.id + + fun validate(input: String): FailedValidationResult? = + if (rule(input)) null else FailedValidationResult(this) + + override fun hashCode(): Int = id.hashCode() +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt new file mode 100644 index 000000000..09c35fa73 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt @@ -0,0 +1,46 @@ +package edu.stanford.spezi.core.design.views.validation + +import edu.stanford.spezi.core.design.component.StringResource +import java.nio.charset.StandardCharsets + +val ValidationRule.Companion.nonEmpty: ValidationRule + get() = ValidationRule( + regex = Regex(".*\\S+.*"), + message = StringResource("VALIDATION_RULE_NON_EMPTY") + ) + +val ValidationRule.Companion.unicodeLettersOnly: ValidationRule + get() = ValidationRule( + rule = { string -> string.all { it.isLetter() } }, + message = StringResource("VALIDATION_RULE_UNICODE_LETTERS") + ) + +val ValidationRule.Companion.asciiLettersOnly: ValidationRule + get() = ValidationRule( + rule = { string -> StandardCharsets.US_ASCII.newEncoder().canEncode(string) }, + message = StringResource("VALIDATION_RULE_UNICODE_LETTERS_ASCII") + ) + +val ValidationRule.Companion.minimalEmail: ValidationRule + get() = ValidationRule( + regex = Regex(".*@.+"), + message = StringResource("VALIDATION_RULE_MINIMAL_EMAIL") + ) + +val ValidationRule.Companion.minimalPassword: ValidationRule + get() = ValidationRule( + regex = Regex(".{8,}"), + message = StringResource("VALIDATION_RULE_PASSWORD_LENGTH 8") + ) + +val ValidationRule.Companion.mediumPassword: ValidationRule + get() = ValidationRule( + regex = Regex(".{10,}"), + message = StringResource("VALIDATION_RULE_PASSWORD_LENGTH 10") + ) + +val ValidationRule.Companion.strongPassword: ValidationRule + get() = ValidationRule( + regex = Regex(".{12,}"), + message = StringResource("VALIDATION_RULE_PASSWORD_LENGTH 12") + ) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt new file mode 100644 index 000000000..15cef5d76 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt @@ -0,0 +1,8 @@ +package edu.stanford.spezi.core.design.views.validation.configuration + +import androidx.compose.runtime.compositionLocalOf +import kotlin.time.Duration.Companion.seconds + +internal val DEFAULT_VALIDATION_DEBOUNCE_DURATION = 0.5.seconds + +val LocalValidationDebounce = compositionLocalOf { DEFAULT_VALIDATION_DEBOUNCE_DURATION } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt new file mode 100644 index 000000000..f02fdc40a --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt @@ -0,0 +1,6 @@ +package edu.stanford.spezi.core.design.views.validation.configuration + +import androidx.compose.runtime.compositionLocalOf +import edu.stanford.spezi.core.design.views.validation.ValidationEngine + +val LocalValidationEngine = compositionLocalOf { null } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt new file mode 100644 index 000000000..30b193192 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt @@ -0,0 +1,9 @@ +package edu.stanford.spezi.core.design.views.validation.configuration + +import androidx.compose.runtime.compositionLocalOf +import edu.stanford.spezi.core.design.views.validation.ValidationEngine +import edu.stanford.spezi.core.design.views.validation.ValidationEngineConfiguration + +val LocalValidationEngineConfiguration = compositionLocalOf { + ValidationEngineConfiguration.noneOf(ValidationEngine.ConfigurationOption::class.java) +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt new file mode 100644 index 000000000..421d54bac --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt @@ -0,0 +1,29 @@ +package edu.stanford.spezi.core.design.views.validation.state + +import androidx.compose.runtime.MutableState +import edu.stanford.spezi.core.design.views.validation.ValidationEngine + +data class CapturedValidationState internal constructor( + internal val engine: ValidationEngine, + private val input: String, + private val isFocused: MutableState, +) { + internal fun moveFocus() { + isFocused.value = true + } + + fun runValidation() { + engine.runValidation(input) + } + + // TODO: Find out whether we can dynamically expose members of ValidationEngine + + override fun hashCode(): Int { + return super.hashCode() + } + + override fun equals(other: Any?): Boolean = + (other as? CapturedValidationState)?.let { + it.engine === engine && it.input == input + } ?: false +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt new file mode 100644 index 000000000..a49c03814 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt @@ -0,0 +1,13 @@ +package edu.stanford.spezi.core.design.views.validation.state + +import androidx.compose.runtime.compositionLocalOf + +internal val LocalCapturedValidationStateEntries = compositionLocalOf { CapturedValidationStateEntries() } + +internal data class CapturedValidationStateEntries( + internal var entries: MutableList = mutableListOf() +) { + fun add(state: CapturedValidationState) { + entries.add(state) + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt new file mode 100644 index 000000000..ab25b0d5e --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt @@ -0,0 +1,15 @@ +package edu.stanford.spezi.core.design.views.validation.state + +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.ValidationRule +import java.util.UUID + +data class FailedValidationResult( + val id: UUID, + val message: StringResource +) { + companion object { + operator fun invoke(rule: ValidationRule) = + FailedValidationResult(rule.id, rule.message) + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt new file mode 100644 index 000000000..814f62cbc --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt @@ -0,0 +1,18 @@ +package edu.stanford.spezi.core.design.views.validation.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.MutableState + +@Composable +fun ReceiveValidation( + state: MutableState, + content: @Composable () -> Unit +) { + val entries = CapturedValidationStateEntries() + CompositionLocalProvider(LocalCapturedValidationStateEntries provides entries) { + content() + // TODO: Possibly wrap this in a change listener instead. + state.value = ValidationContext(entries.entries) + } +} \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt new file mode 100644 index 000000000..80bf1c1ec --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt @@ -0,0 +1,43 @@ +package edu.stanford.spezi.core.design.views.validation.state + +data class ValidationContext internal constructor( + private val entries: List = emptyList() +) : Iterable { + val allInputValid: Boolean get() = + entries.all { it.engine.inputValid } + + val allValidationResults: List get() = + entries.fold(emptyList()) { acc, entry -> acc + entry.engine.validationResults } + + val allDisplayedValidationResults: List get() = + entries.fold(emptyList()) { acc, entry -> acc + entry.engine.displayedValidationResults } + + val isDisplayingValidationErrors: Boolean get() = + entries.any { it.engine.isDisplayingValidationErrors } + + override fun iterator(): Iterator = entries.iterator() + + val isEmpty: Boolean + get() = entries.isEmpty() + + private fun collectFailedValidations(): List { + return mapNotNull { state -> + state.runValidation() + + if (state.engine.inputValid) state else null + } + } + + // TODO: Originally called validateSubviews, but renamed to avoid using "view" on Android + fun validateHierarchy(switchFocus: Boolean = true): Boolean { + val failedFields = collectFailedValidations() + + return failedFields.firstOrNull()?.let { + if (switchFocus) { + it.moveFocus() + } + + false + } ?: true + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt new file mode 100644 index 000000000..a3bb875fe --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt @@ -0,0 +1,4 @@ +package edu.stanford.spezi.core.design.views.validation.views + +class ValidationResultsComposable { +} \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt new file mode 100644 index 000000000..8790dc34a --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -0,0 +1,4 @@ +package edu.stanford.spezi.core.design.views.validation.views + +class VerifiableTextField { +} \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt new file mode 100644 index 000000000..2c96fa2a8 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt @@ -0,0 +1,6 @@ +package edu.stanford.spezi.core.design.views.views.compositionLocal + +import androidx.compose.runtime.compositionLocalOf +import kotlin.time.Duration.Companion.milliseconds + +val LocalProcessingDebounceDuration = compositionLocalOf { 150.milliseconds } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt new file mode 100644 index 000000000..230ba0101 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt @@ -0,0 +1,74 @@ +package edu.stanford.spezi.core.design.views.views.layout + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.theme.ThemePreviews + +@Composable +fun DescriptionGridRow( + description: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .alignByBaseline() + .weight(1f, fill = false) + ) { + description() + } + + Box( + modifier = Modifier + .alignByBaseline() + .fillMaxWidth() + .weight(1f) + ) { + content() + } + } +} + +@ThemePreviews +@Composable +private fun DescriptionGridRowPreviews() { + Column { + DescriptionGridRow(description = { + Text("Description") + }) { + Text("Content") + } + + HorizontalDivider() + + DescriptionGridRow(description = { + Text("Description") + }) { + Text("Content") + } + + HorizontalDivider() + + DescriptionGridRow(description = { + Text("Description") + }) { + Text("Content") + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt new file mode 100644 index 000000000..e252a1615 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt @@ -0,0 +1,5 @@ +package edu.stanford.spezi.core.design.views.views.model + +interface OperationState { + val representation: ViewState +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt new file mode 100644 index 000000000..a73b14109 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt @@ -0,0 +1,17 @@ +package edu.stanford.spezi.core.design.views.views.model + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import edu.stanford.spezi.core.design.component.StringResource + +sealed interface ViewState { + data object Idle : ViewState + data object Processing : ViewState + data class Error(val throwable: Throwable?) : ViewState + + val errorTitle: String + @Composable @ReadOnlyComposable get() = StringResource("Error").text() + + val errorDescription: String + @Composable @ReadOnlyComposable get() = if (this is Error) throwable?.localizedMessage ?: "" else "" +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/OnChangeListener.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/OnChangeListener.kt new file mode 100644 index 000000000..004d8b9be --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/OnChangeListener.kt @@ -0,0 +1,22 @@ +package edu.stanford.spezi.core.design.views.views.viewModifier + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +@Composable +fun OnChangeListener(state: T, initial: Boolean = false, block: (T?) -> Unit) { + if (initial) { + val previousValue = remember { mutableStateOf(null) } + + if (state != previousValue) { + block(previousValue.value) + } + } else { + val previousValue = remember { mutableStateOf(state) } + + if (state != previousValue) { + block(previousValue.value) + } + } +} \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt new file mode 100644 index 000000000..5d5ee58d9 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt @@ -0,0 +1,16 @@ +package edu.stanford.spezi.core.design.views.views.viewModifier.viewState + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.core.design.views.views.model.OperationState + +@Composable +fun OperationStateAlert( + state: MutableState +) { + val viewState = remember { mutableStateOf(state.value.representation) } + MapOperationStateToViewState(state.value, viewState) + ViewStateAlert(viewState) +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt new file mode 100644 index 000000000..43455d706 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt @@ -0,0 +1,34 @@ +package edu.stanford.spezi.core.design.views.views.viewModifier.viewState + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import edu.stanford.spezi.core.design.views.views.model.ViewState + +@Composable +fun ViewStateAlert(state: MutableState) { + if (state.value is ViewState.Error) { + AlertDialog( + title = { + Text(text = state.value.errorTitle) + }, + text = { + Text(text = state.value.errorDescription) + }, + onDismissRequest = { + state.value = ViewState.Idle + }, + confirmButton = { + TextButton( + onClick = { + state.value = ViewState.Idle + } + ) { + Text("Okay") + } + } + ) + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt new file mode 100644 index 000000000..6348b93bc --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt @@ -0,0 +1,14 @@ +package edu.stanford.spezi.core.design.views.views.viewModifier.viewState + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import edu.stanford.spezi.core.design.views.views.model.OperationState +import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.core.design.views.views.viewModifier.OnChangeListener + +@Composable +fun MapOperationStateToViewState(state: State, viewState: MutableState) { + OnChangeListener(state) { + viewState.value = state.representation + } +} \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt new file mode 100644 index 000000000..639ba9478 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt @@ -0,0 +1,62 @@ +package edu.stanford.spezi.core.design.views.views.views.button + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import edu.stanford.spezi.core.design.component.Button +import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.core.utils.UUID +import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +private enum class SuspendButtonState { + IDLE, DISABLED, DISABLED_AND_PROCESSING +} + +@Composable +fun SuspendButton( + state: MutableState, + action: suspend () -> Unit, + label: @Composable () -> Unit +) { + val buttonState = remember { mutableStateOf(SuspendButtonState.IDLE) } + val coroutineScope = rememberCoroutineScope() + val debounceScope = rememberCoroutineScope() + + DisposableEffect(remember { UUID() }) { + onDispose { + coroutineScope.cancel() + } + } + + Button( + onClick = { + if (state.value == ViewState.Processing) return@Button + buttonState.value = SuspendButtonState.DISABLED + + // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? + state.value = ViewState.Processing + + coroutineScope.launch { + runCatching { + action() + if (state.value != ViewState.Idle) { + // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? + state.value = ViewState.Idle + } + }.onFailure { + state.value = ViewState.Error(it) + } + + buttonState.value = SuspendButtonState.IDLE + } + }, + enabled = !coroutineScope.isActive + ) { + label() + } +} diff --git a/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt b/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt new file mode 100644 index 000000000..2c78297f4 --- /dev/null +++ b/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt @@ -0,0 +1,34 @@ +package edu.stanford.spezi.core.design + +import com.google.common.truth.Truth.assertThat +import edu.stanford.spezi.core.design.views.validation.ValidationEngine +import edu.stanford.spezi.core.design.views.validation.ValidationRule +import edu.stanford.spezi.core.design.views.validation.nonEmpty +import kotlinx.coroutines.CoroutineScope +import org.junit.Test + +class SpeziValidationTest { + + @Test + fun testValidationDebounce() { + val engine = ValidationEngine(rules = listOf(ValidationRule.nonEmpty)) + + engine.submit("Valid") + assertThat(engine.inputValid).isTrue() + assertThat(engine.validationResults).isEmpty() + + engine.submit("", debounce = true) + assertThat(engine.inputValid).isTrue() + assertThat(engine.validationResults).isEmpty() + + Thread.sleep(1_000) + + assertThat(engine.inputValid).isFalse() + assertThat(engine.validationResults).hasSize(1) + + engine.submit("Valid", debounce = true) + assertThat(engine.inputValid).isTrue() // valid state is reported instantly + assertThat(engine.validationResults).isEmpty() + } + +} diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt deleted file mode 100644 index d2987a340..000000000 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt +++ /dev/null @@ -1,22 +0,0 @@ -package edu.stanford.spezi.modules.contact.model - -data class PersonNameComponents( - val namePrefix: String? = null, - val givenName: String? = null, - val middleName: String? = null, - val familyName: String? = null, - val nameSuffix: String? = null, - val nickname: String? = null, -) { - fun formatted(): String { - val components = listOfNotNull( - namePrefix, - givenName, - nickname?.let { "\"$it\"" }, - middleName, - familyName, - nameSuffix - ) - return components.joinToString(" ") - } -} From 11efb05e54a6a8434685faa6dea3b7049a774146 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 10 Nov 2024 10:31:13 -0800 Subject: [PATCH 04/51] intermediate --- .../spezi/core/design/ValidationTest.kt | 44 ++++++++++ .../composables/DefaultValidationRules.kt | 38 ++++++++ .../composables/FocusValidationRules.kt | 63 +++++++++++++ .../DefaultValidationRulesSimulator.kt | 9 ++ .../FocusValidationRulesSimulator.kt | 50 +++++++++++ .../personalInfo/UserProfileComposable.kt | 10 ++- .../views/personalInfo/fields/NameFieldRow.kt | 3 - .../personalInfo/fields/NameTextField.kt | 1 - .../views/validation/ValidationEngine.kt | 59 ++++++++----- .../views/validation/ValidationModifier.kt | 10 +-- .../design/views/validation/ValidationRule.kt | 3 +- .../validation/ValidationRuleDefaults.kt | 4 +- .../state/CapturedValidationState.kt | 4 +- .../state/CapturedValidationStateEntries.kt | 6 +- .../state/FailedValidationResult.kt | 3 + .../validation/state/ReceiveValidation.kt | 12 ++- .../validation/state/ValidationContext.kt | 17 ++-- .../views/ValidationResultsComposable.kt | 24 ++++- .../validation/views/VerifiableTextField.kt | 88 ++++++++++++++++++- .../views/viewModifier/OnChangeListener.kt | 22 ----- .../viewModifier/viewState/ViewStateMapper.kt | 6 +- .../simulator/ContactComposableSimulator.kt | 2 +- 22 files changed, 396 insertions(+), 82 deletions(-) create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/ValidationTest.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/DefaultValidationRules.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/FocusValidationRules.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/DefaultValidationRulesSimulator.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/FocusValidationRulesSimulator.kt delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/OnChangeListener.kt diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/ValidationTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/ValidationTest.kt new file mode 100644 index 000000000..11e20aa2c --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/ValidationTest.kt @@ -0,0 +1,44 @@ +package edu.stanford.spezi.core.design + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import edu.stanford.spezi.core.design.composables.FocusValidationRules +import edu.stanford.spezi.core.design.simulator.FocusValidationRulesSimulator +import edu.stanford.spezi.core.design.views.validation.state.ValidationContext +import kotlinx.coroutines.delay +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class ValidationTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun init() { + composeTestRule.setContent { + FocusValidationRules() + } + } + + @Test + fun testValidationWithFocus() { + focusValidationRules { + assertHasEngines(true) + assertInputValid(false) + assertPasswordMessageExists(false) + assertEmptyMessageExists(false) + clickValidateButton() + assertLastState(false) + assertPasswordMessageExists(true) + // assertEmptyMessageExists(true) + } + } + + private fun focusValidationRules(block: FocusValidationRulesSimulator.() -> Unit) { + FocusValidationRulesSimulator(composeTestRule).apply(block) + } + +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/DefaultValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/DefaultValidationRules.kt new file mode 100644 index 000000000..1f57e3b28 --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/DefaultValidationRules.kt @@ -0,0 +1,38 @@ +package edu.stanford.spezi.core.design.composables + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.Validate +import edu.stanford.spezi.core.design.views.validation.ValidationRule +import edu.stanford.spezi.core.design.views.validation.asciiLettersOnly +import edu.stanford.spezi.core.design.views.validation.mediumPassword +import edu.stanford.spezi.core.design.views.validation.minimalEmail +import edu.stanford.spezi.core.design.views.validation.minimalPassword +import edu.stanford.spezi.core.design.views.validation.nonEmpty +import edu.stanford.spezi.core.design.views.validation.strongPassword +import edu.stanford.spezi.core.design.views.validation.unicodeLettersOnly +import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField + +@Composable +fun DefaultValidationRules() { + val input = remember { mutableStateOf("") } + val rules = remember { + listOf( + ValidationRule.nonEmpty, + ValidationRule.unicodeLettersOnly, + ValidationRule.asciiLettersOnly, + ValidationRule.minimalEmail, + ValidationRule.minimalPassword, + ValidationRule.mediumPassword, + ValidationRule.strongPassword + ) + } + Validate(input.value, rules) { + VerifiableTextField( + StringResource("Field"), + text = input + ) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/FocusValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/FocusValidationRules.kt new file mode 100644 index 000000000..57acecb0e --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/FocusValidationRules.kt @@ -0,0 +1,63 @@ +package edu.stanford.spezi.core.design.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.Validate +import edu.stanford.spezi.core.design.views.validation.ValidationRule +import edu.stanford.spezi.core.design.views.validation.minimalPassword +import edu.stanford.spezi.core.design.views.validation.nonEmpty +import edu.stanford.spezi.core.design.views.validation.state.ReceiveValidation +import edu.stanford.spezi.core.design.views.validation.state.ValidationContext +import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField + +enum class Field { + INPUT, NON_EMPTY_INPUT +} + +@Composable +fun FocusValidationRules() { + val input = remember { mutableStateOf("") } + val nonEmptyInput = remember { mutableStateOf("") } + val validationContext = remember { mutableStateOf(ValidationContext()) } + val lastValid = remember { mutableStateOf(null) } + val switchFocus = remember { mutableStateOf(false) } + + ReceiveValidation(validationContext) { + Column { + Text("Has Engines: ${if (!validationContext.value.isEmpty) "Yes" else "No"}") + Text("Input Valid: ${if (validationContext.value.allInputValid) "Yes" else "No"}") + lastValid.value?.let { lastValid -> + Text("Last state: ${if (lastValid) "valid" else "invalid"}") + } + Button( + onClick = { + val newLastValid = validationContext.value + .validateHierarchy(switchFocus.value) + lastValid.value = newLastValid + } + ) { + Text("Validate") + } + Row { + Text("Switch Focus") + Switch(switchFocus.value, onCheckedChange = { switchFocus.value = it }) + } + + Validate(input.value, rules = listOf(ValidationRule.minimalPassword)) { + VerifiableTextField(StringResource(Field.INPUT.name), input) + } + + Validate(nonEmptyInput.value, rules = listOf(ValidationRule.nonEmpty)) { + VerifiableTextField(StringResource(Field.NON_EMPTY_INPUT.name), nonEmptyInput) + } + } + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/DefaultValidationRulesSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/DefaultValidationRulesSimulator.kt new file mode 100644 index 000000000..36415d43d --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/DefaultValidationRulesSimulator.kt @@ -0,0 +1,9 @@ +package edu.stanford.spezi.core.design.simulator + +import androidx.compose.ui.test.junit4.ComposeTestRule + +class DefaultValidationRulesSimulator( + private val composeTestRule: ComposeTestRule, +) { + +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/FocusValidationRulesSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/FocusValidationRulesSimulator.kt new file mode 100644 index 000000000..78231742d --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/FocusValidationRulesSimulator.kt @@ -0,0 +1,50 @@ +package edu.stanford.spezi.core.design.simulator + +import androidx.compose.material3.Text +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class FocusValidationRulesSimulator( + private val composeTestRule: ComposeTestRule, +) { + private val passwordMessage = "Your password must be at least 8 characters long." + private val emptyMessage = "This field cannot be empty." + + fun assertHasEngines(hasEngines: Boolean) { + composeTestRule + .onNodeWithText("Has Engines: ${if (hasEngines) "Yes" else "No"}") + .assertExists() + } + + fun assertInputValid(inputValid: Boolean) { + composeTestRule + .onNodeWithText("Input Valid: ${if (inputValid) "Yes" else "No"}") + .assertExists() + } + + fun assertPasswordMessageExists(exists: Boolean) { + val node = composeTestRule.onNodeWithText(passwordMessage) + if (exists) node.assertExists() else node.assertDoesNotExist() + } + + fun assertEmptyMessageExists(exists: Boolean) { + val node = composeTestRule.onNodeWithText(emptyMessage) + if (exists) node.assertExists() else node.assertDoesNotExist() + } + + fun clickValidateButton() { + composeTestRule + .onNodeWithText("Validate") + .assertHasClickAction() + .performClick() + } + + fun assertLastState(valid: Boolean) { + composeTestRule + .onNodeWithText("Last state: ${if (valid) "valid" else "invalid"}") + .assertExists() + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt index 9cf2a82c8..b3b1dfc94 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import edu.stanford.spezi.core.design.component.ImageResource +import edu.stanford.spezi.core.design.component.ImageResourceComposable import edu.stanford.spezi.core.design.theme.Colors import edu.stanford.spezi.core.design.theme.lighten import kotlin.math.min @@ -28,9 +30,9 @@ import kotlin.math.min fun UserProfileComposable( modifier: Modifier = Modifier, name: PersonNameComponents, - imageLoader: suspend () -> ImageVector? = { null }, // TODO: Use ImageResource instead! + imageLoader: suspend () -> ImageResource? = { null }, ) { - val image = remember { mutableStateOf(null) } + val image = remember { mutableStateOf(null) } val size = remember { mutableStateOf(IntSize.Zero) } LaunchedEffect(Unit) { @@ -41,9 +43,9 @@ fun UserProfileComposable( val sideLength = min(size.value.height, size.value.width).dp Box(modifier.size(sideLength, sideLength), contentAlignment = Alignment.Center) { image.value?.let { - Image( + ImageResourceComposable( it, - null, + "", // TODO: Add contentDescription to ImageResource directly? Modifier .clip(CircleShape) .background(Colors.background, CircleShape) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt index be99e55f0..48102fe1f 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt @@ -1,10 +1,7 @@ package edu.stanford.spezi.core.design.views.personalInfo.fields import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid -import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt index c60be6459..5ffacc1cc 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt @@ -1,6 +1,5 @@ package edu.stanford.spezi.core.design.views.personalInfo.fields -import android.app.Person import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.material3.TextField diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt index f20eb35c7..42f50e87d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt @@ -1,58 +1,69 @@ package edu.stanford.spezi.core.design.views.validation -import android.provider.Settings.Global +import androidx.compose.runtime.mutableStateOf import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job -import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.util.EnumSet import kotlin.time.Duration -typealias ValidationEngineConfiguration = EnumSet - -class ValidationEngine( - val rules: List, - var debounceDuration: Duration = DEFAULT_VALIDATION_DEBOUNCE_DURATION, - var configuration: ValidationEngineConfiguration = ValidationEngineConfiguration.noneOf(ConfigurationOption::class.java), -) { - private enum class Source { - SUBMIT, MANUAL - } +internal typealias ValidationEngineConfiguration = EnumSet +interface ValidationEngine { enum class ConfigurationOption { HIDE_FAILED_VALIDATION_ON_EMPTY_SUBMIT, CONSIDER_NO_INPUT_AS_VALID, } - var validationResults: List = emptyList() - private set + val rules: List + val inputValid: Boolean + val validationResults: List + val isDisplayingValidationErrors: Boolean + val displayedValidationResults: List + var debounceDuration: Duration + + fun submit(input: String, debounce: Boolean = false) + fun runValidation(input: String) +} + +internal class ValidationEngineImpl( + override val rules: List, + override var debounceDuration: Duration = DEFAULT_VALIDATION_DEBOUNCE_DURATION, + var configuration: ValidationEngineConfiguration = + ValidationEngineConfiguration.noneOf(ValidationEngine.ConfigurationOption::class.java), +) : ValidationEngine { + private enum class Source { + SUBMIT, MANUAL + } + + private var validationResultsState = mutableStateOf(emptyList()) + + override val validationResults get() = validationResultsState.value private var computedInputValid: Boolean? = null - val inputValid: Boolean get() = - computedInputValid ?: configuration.contains(ConfigurationOption.CONSIDER_NO_INPUT_AS_VALID) + override val inputValid: Boolean get() = + computedInputValid ?: configuration.contains(ValidationEngine.ConfigurationOption.CONSIDER_NO_INPUT_AS_VALID) private var source: Source? = null private var inputWasEmpty = true - val isDisplayingValidationErrors: Boolean get() { + override val isDisplayingValidationErrors: Boolean get() { val gotResults = validationResults.isNotEmpty() - if (configuration.contains(ConfigurationOption.HIDE_FAILED_VALIDATION_ON_EMPTY_SUBMIT)) { + if (configuration.contains(ValidationEngine.ConfigurationOption.HIDE_FAILED_VALIDATION_ON_EMPTY_SUBMIT)) { return gotResults && (source == Source.MANUAL || !inputWasEmpty) } return gotResults } - val displayedValidationResults: List get() = + override val displayedValidationResults: List get() = if (isDisplayingValidationErrors) validationResults else emptyList() private var debounceJob: Job? = null @@ -75,11 +86,11 @@ class ValidationEngine( this.source = source this.inputWasEmpty = input.isEmpty() - this.validationResults = computeFailedValidations(input) + this.validationResultsState.value = computeFailedValidations(input) this.computedInputValid = validationResults.isEmpty() } - fun submit(input: String, debounce: Boolean = false) { + override fun submit(input: String, debounce: Boolean) { if (!debounce || computedInputValid == false) { computeValidation(input, Source.SUBMIT) } else { @@ -89,7 +100,7 @@ class ValidationEngine( } } - fun runValidation(input: String) { + override fun runValidation(input: String) { computeValidation(input, Source.MANUAL) } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt index 560a39d90..02a87f526 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt @@ -3,6 +3,7 @@ package edu.stanford.spezi.core.design.views.validation import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource @@ -37,23 +38,22 @@ fun Validate( rules: List, content: @Composable () -> Unit, ) { - val previousInput = remember { mutableStateOf(input) } val validationDebounce = LocalValidationDebounce.current val previousValidationDebounce = remember { mutableStateOf(null) } val validationEngineConfiguration = LocalValidationEngineConfiguration.current val previousValidationEngineConfiguration = remember { mutableStateOf(null) } - val engine = remember { ValidationEngine(rules, validationDebounce, validationEngineConfiguration) } + val engine = remember { ValidationEngineImpl(rules, validationDebounce, validationEngineConfiguration) } - if (input != previousInput.value) { + LaunchedEffect(input) { engine.submit(input, debounce = true) } - if (validationDebounce != previousValidationDebounce.value) { + LaunchedEffect(validationDebounce) { engine.debounceDuration = validationDebounce previousValidationDebounce.value = validationDebounce } - if (validationEngineConfiguration != previousValidationEngineConfiguration.value) { + LaunchedEffect(validationEngineConfiguration) { engine.configuration = validationEngineConfiguration previousValidationEngineConfiguration.value = validationEngineConfiguration } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt index 93fbdd97c..706ac77b5 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt @@ -2,10 +2,11 @@ package edu.stanford.spezi.core.design.views.validation import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult +import edu.stanford.spezi.core.utils.UUID import java.util.UUID data class ValidationRule internal constructor( - val id: UUID = UUID.randomUUID(), + val id: UUID = UUID(), val rule: (String) -> Boolean, val message: StringResource, val effect: CascadingValidationEffect = CascadingValidationEffect.CONTINUE diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt index 09c35fa73..a6aa86bc7 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt @@ -6,7 +6,7 @@ import java.nio.charset.StandardCharsets val ValidationRule.Companion.nonEmpty: ValidationRule get() = ValidationRule( regex = Regex(".*\\S+.*"), - message = StringResource("VALIDATION_RULE_NON_EMPTY") + message = StringResource("This field cannot be empty.") ) val ValidationRule.Companion.unicodeLettersOnly: ValidationRule @@ -30,7 +30,7 @@ val ValidationRule.Companion.minimalEmail: ValidationRule val ValidationRule.Companion.minimalPassword: ValidationRule get() = ValidationRule( regex = Regex(".{8,}"), - message = StringResource("VALIDATION_RULE_PASSWORD_LENGTH 8") + message = StringResource("Your password must be at least 8 characters long.") ) val ValidationRule.Companion.mediumPassword: ValidationRule diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt index 421d54bac..5288bb5f7 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt @@ -4,10 +4,10 @@ import androidx.compose.runtime.MutableState import edu.stanford.spezi.core.design.views.validation.ValidationEngine data class CapturedValidationState internal constructor( - internal val engine: ValidationEngine, + private val engine: ValidationEngine, private val input: String, private val isFocused: MutableState, -) { +) : ValidationEngine by engine { internal fun moveFocus() { isFocused.value = true } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt index a49c03814..53896bc3d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt @@ -5,9 +5,11 @@ import androidx.compose.runtime.compositionLocalOf internal val LocalCapturedValidationStateEntries = compositionLocalOf { CapturedValidationStateEntries() } internal data class CapturedValidationStateEntries( - internal var entries: MutableList = mutableListOf() + private var _entries: MutableList = mutableListOf() ) { + val entries: List get() = _entries + fun add(state: CapturedValidationState) { - entries.add(state) + _entries.add(state) } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt index ab25b0d5e..50ec874b8 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt @@ -12,4 +12,7 @@ data class FailedValidationResult( operator fun invoke(rule: ValidationRule) = FailedValidationResult(rule.id, rule.message) } + + override fun equals(other: Any?) = (other as? FailedValidationResult)?.id == id + override fun hashCode() = id.hashCode() } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt index 814f62cbc..94ce4e850 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt @@ -2,17 +2,21 @@ package edu.stanford.spezi.core.design.views.validation.state import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @Composable fun ReceiveValidation( state: MutableState, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { + // This is not remembered on purpose, since we are re-evaluating the validation here. val entries = CapturedValidationStateEntries() CompositionLocalProvider(LocalCapturedValidationStateEntries provides entries) { content() - // TODO: Possibly wrap this in a change listener instead. - state.value = ValidationContext(entries.entries) + + LaunchedEffect(entries.entries) { + state.value = ValidationContext(entries.entries) + } } -} \ No newline at end of file +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt index 80bf1c1ec..bcc392366 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt @@ -4,16 +4,16 @@ data class ValidationContext internal constructor( private val entries: List = emptyList() ) : Iterable { val allInputValid: Boolean get() = - entries.all { it.engine.inputValid } + entries.all { it.inputValid } val allValidationResults: List get() = - entries.fold(emptyList()) { acc, entry -> acc + entry.engine.validationResults } + entries.fold(emptyList()) { acc, entry -> acc + entry.validationResults } val allDisplayedValidationResults: List get() = - entries.fold(emptyList()) { acc, entry -> acc + entry.engine.displayedValidationResults } + entries.fold(emptyList()) { acc, entry -> acc + entry.displayedValidationResults } val isDisplayingValidationErrors: Boolean get() = - entries.any { it.engine.isDisplayingValidationErrors } + entries.any { it.isDisplayingValidationErrors } override fun iterator(): Iterator = entries.iterator() @@ -24,7 +24,7 @@ data class ValidationContext internal constructor( return mapNotNull { state -> state.runValidation() - if (state.engine.inputValid) state else null + if (!state.inputValid) state else null } } @@ -40,4 +40,11 @@ data class ValidationContext internal constructor( false } ?: true } + + override fun hashCode(): Int { + return super.hashCode() + } + + override fun equals(other: Any?): Boolean = + (other as? ValidationContext)?.entries == entries } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt index a3bb875fe..8be8a126a 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt @@ -1,4 +1,26 @@ package edu.stanford.spezi.core.design.views.validation.views -class ValidationResultsComposable { +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import edu.stanford.spezi.core.design.theme.TextStyles +import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult + +@Composable +fun ValidationResultsComposable( + results: List +) { + Column( + horizontalAlignment = Alignment.Start + ) { + for (result in results) { + Text( + result.message.text(), + style = TextStyles.labelSmall, + color = Color.Red, + ) + } + } } \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index 8790dc34a..985151613 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -1,4 +1,88 @@ package edu.stanford.spezi.core.design.views.validation.views -class VerifiableTextField { -} \ No newline at end of file +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine + +enum class TextFieldType { + TEXT, SECURE +} + +@Composable +fun VerifiableTextField( + label: StringResource, + text: MutableState, + type: TextFieldType = TextFieldType.TEXT, + disableAutocorrection: Boolean = false, + footer: @Composable () -> Unit = {} +) { + VerifiableTextField( + text, + type, + disableAutocorrection = disableAutocorrection, + { Text(label.text()) }, + footer + ) +} + +@Composable +fun VerifiableTextField( + text: MutableState, + type: TextFieldType = TextFieldType.TEXT, + disableAutocorrection: Boolean = false, + label: @Composable () -> Unit, + footer: @Composable () -> Unit = {} +) { + val validationEngine = LocalValidationEngine.current + + Column { + // TODO: Check if this is really equivalent, + // since iOS specifies this as a completely separate type + // and there we only have this visualTransformation property + when (type) { + TextFieldType.TEXT -> { + TextField( + text.value, + onValueChange = { text.value = it }, + label = label, + keyboardOptions = KeyboardOptions( + autoCorrect = !disableAutocorrection + ), + ) + } + TextFieldType.SECURE -> { + TextField( + text.value, + onValueChange = { text.value = it }, + label = label, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + autoCorrect = !disableAutocorrection + ), + visualTransformation = PasswordVisualTransformation() + ) + } + } + + Row { + validationEngine?.let { + ValidationResultsComposable(it.displayedValidationResults) + + Spacer(Modifier.fillMaxWidth()) + } + + footer() + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/OnChangeListener.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/OnChangeListener.kt deleted file mode 100644 index 004d8b9be..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/OnChangeListener.kt +++ /dev/null @@ -1,22 +0,0 @@ -package edu.stanford.spezi.core.design.views.views.viewModifier - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember - -@Composable -fun OnChangeListener(state: T, initial: Boolean = false, block: (T?) -> Unit) { - if (initial) { - val previousValue = remember { mutableStateOf(null) } - - if (state != previousValue) { - block(previousValue.value) - } - } else { - val previousValue = remember { mutableStateOf(state) } - - if (state != previousValue) { - block(previousValue.value) - } - } -} \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt index 6348b93bc..1ca174b1d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt @@ -1,14 +1,14 @@ package edu.stanford.spezi.core.design.views.views.viewModifier.viewState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import edu.stanford.spezi.core.design.views.views.model.OperationState import edu.stanford.spezi.core.design.views.views.model.ViewState -import edu.stanford.spezi.core.design.views.views.viewModifier.OnChangeListener @Composable fun MapOperationStateToViewState(state: State, viewState: MutableState) { - OnChangeListener(state) { + LaunchedEffect(state) { viewState.value = state.representation } -} \ No newline at end of file +} diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt index 69eaadcc3..7db3a40cc 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt @@ -10,11 +10,11 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.test.platform.app.InstrumentationRegistry import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import edu.stanford.spezi.core.testing.assertImageIdentifier import edu.stanford.spezi.core.testing.onNodeWithIdentifier import edu.stanford.spezi.modules.contact.ContactComposableTestIdentifier import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.formatted class ContactComposableSimulator( From 5fa1c8d322b6242459dd805049ba33a786324346 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 10 Nov 2024 13:32:25 -0800 Subject: [PATCH 05/51] Update SuspendButton implementation --- .../personalInfo/UserProfileComposable.kt | 2 - .../views/views/button/ProcessingOverlay.kt | 40 +++++++++++++++++++ .../views/views/views/button/SuspendButton.kt | 27 +++++++++---- 3 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt index b3b1dfc94..b7da19e68 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt @@ -1,6 +1,5 @@ package edu.stanford.spezi.core.design.views.personalInfo -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio @@ -15,7 +14,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt new file mode 100644 index 000000000..916d9cf6c --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt @@ -0,0 +1,40 @@ +package edu.stanford.spezi.core.design.views.views.views.button + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import edu.stanford.spezi.core.design.views.views.model.ViewState + +@Composable +fun ProcessingOverlay( + viewState: ViewState, + processingContent: @Composable () -> Unit = { CircularProgressIndicator() }, + content: @Composable () -> Unit, +) { + ProcessingOverlay( + isProcessing = viewState == ViewState.Processing, + processingContent = processingContent, + content = content, + ) +} + +@Composable +fun ProcessingOverlay( + isProcessing: Boolean, + processingContent: @Composable () -> Unit = { CircularProgressIndicator() }, + content: @Composable () -> Unit, + ) { + Box { + Box(Modifier.alpha(if (isProcessing) 0f else 1f)) { + content() + } + + if (isProcessing) { + Box(Modifier.matchParentSize()) { + processingContent() + } + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt index 639ba9478..690099959 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt @@ -7,9 +7,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import edu.stanford.spezi.core.design.component.Button +import edu.stanford.spezi.core.design.views.views.compositionLocal.LocalProcessingDebounceDuration import edu.stanford.spezi.core.design.views.views.model.ViewState import edu.stanford.spezi.core.utils.UUID import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -26,37 +28,46 @@ fun SuspendButton( val buttonState = remember { mutableStateOf(SuspendButtonState.IDLE) } val coroutineScope = rememberCoroutineScope() val debounceScope = rememberCoroutineScope() - - DisposableEffect(remember { UUID() }) { - onDispose { - coroutineScope.cancel() - } - } + val processingDebounceDuration = LocalProcessingDebounceDuration.current + val externallyProcessing = buttonState.value == SuspendButtonState.IDLE && state.value == ViewState.Processing Button( + enabled = buttonState.value == SuspendButtonState.IDLE && !externallyProcessing, onClick = { if (state.value == ViewState.Processing) return@Button buttonState.value = SuspendButtonState.DISABLED + val debounceJob = debounceScope.launch { + delay(processingDebounceDuration) + + if (isActive) { + buttonState.value = SuspendButtonState.DISABLED_AND_PROCESSING + } + } + // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? state.value = ViewState.Processing coroutineScope.launch { runCatching { action() + debounceJob.cancel() + if (state.value != ViewState.Idle) { // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? state.value = ViewState.Idle } }.onFailure { + debounceJob.cancel() state.value = ViewState.Error(it) } buttonState.value = SuspendButtonState.IDLE } }, - enabled = !coroutineScope.isActive ) { - label() + ProcessingOverlay(buttonState.value == SuspendButtonState.DISABLED_AND_PROCESSING || externallyProcessing) { + label() + } } } From 37159b842ecc51d40055bd96bae5458cc8a3fdf8 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 10 Nov 2024 16:54:22 -0800 Subject: [PATCH 06/51] Update tests for PersonalInfo --- .../design/personalInfo/NameFieldsTest.kt | 36 +++++++++++++++++++ .../design/personalInfo/UserProfileTest.kt | 34 ++++++++++++++++++ .../composables/NameFieldsTestComposable.kt | 28 +++++++++++++++ .../composables/UserProfileTestComposable.kt | 27 ++++++++++++++ .../simulators/NameFieldsTestSimulator.kt | 22 ++++++++++++ .../simulators/UserProfileTestSimulator.kt | 27 ++++++++++++++ .../DefaultValidationRulesSimulator.kt | 9 ----- .../core/design/{ => views}/ValidationTest.kt | 13 +++---- .../composables/DefaultValidationRules.kt | 2 +- .../composables/FocusValidationRules.kt | 3 +- .../FocusValidationRulesSimulator.kt | 4 +-- .../personalInfo/UserProfileComposable.kt | 10 ++---- .../design/views/validation/ValidationRule.kt | 2 +- .../state/CapturedValidationStateEntries.kt | 6 ++-- .../state/FailedValidationResult.kt | 2 +- .../validation/state/ValidationContext.kt | 2 +- .../views/ValidationResultsComposable.kt | 4 +-- .../validation/views/VerifiableTextField.kt | 4 +-- .../viewState/OperationStateAlert.kt | 2 +- .../views/views/button/ProcessingOverlay.kt | 2 +- .../views/views/views/button/SuspendButton.kt | 4 +-- .../spezi/core/design/SpeziValidationTest.kt | 6 ++-- 22 files changed, 198 insertions(+), 51 deletions(-) create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/NameFieldsTestSimulator.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt delete mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/DefaultValidationRulesSimulator.kt rename core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/{ => views}/ValidationTest.kt (66%) rename core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/{ => views}/composables/DefaultValidationRules.kt (96%) rename core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/{ => views}/composables/FocusValidationRules.kt (96%) rename core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/{simulator => views/simulators}/FocusValidationRulesSimulator.kt (92%) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt new file mode 100644 index 000000000..4e339adce --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt @@ -0,0 +1,36 @@ +package edu.stanford.spezi.core.design.personalInfo + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.core.design.personalInfo.composables.NameFieldsTestComposable +import edu.stanford.spezi.core.design.personalInfo.simulators.NameFieldsTestSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class NameFieldsTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun init() { + composeTestRule.setContent { + NameFieldsTestComposable() + } + } + + @Test + fun testNameFields() { + nameFields { + assertTextExists("First Name") + assertTextExists("Last Name") + + enterText("enter your first name", "Leland") + enterText("enter your last name", "Stanford") + } + } + + private fun nameFields(block: NameFieldsTestSimulator.() -> Unit) { + NameFieldsTestSimulator(composeTestRule).apply(block) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt new file mode 100644 index 000000000..7385cfd58 --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt @@ -0,0 +1,34 @@ +package edu.stanford.spezi.core.design.personalInfo + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.core.design.personalInfo.composables.UserProfileTestComposable +import edu.stanford.spezi.core.design.personalInfo.simulators.UserProfileTestSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class UserProfileTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun init() { + composeTestRule.setContent { + UserProfileTestComposable() + } + } + + @Test + fun testUserProfile() { + userProfile { + assertUserInitialsExists(true, "PS") + assertUserInitialsExists(false, "LS") + assertImageExists() + } + } + + private fun userProfile(block: UserProfileTestSimulator.() -> Unit) { + UserProfileTestSimulator(composeTestRule).apply(block) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt new file mode 100644 index 000000000..9bcf547bb --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt @@ -0,0 +1,28 @@ +package edu.stanford.spezi.core.design.personalInfo.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.fields.NameFieldRow + +@Composable +fun NameFieldsTestComposable() { + val name = remember { mutableStateOf(PersonNameComponents()) } + + Column { + NameFieldRow(StringResource("First Name"), name, PersonNameComponents::givenName) { + Text("enter your first name") + } + + HorizontalDivider() + + NameFieldRow(StringResource("Last Name"), name, PersonNameComponents::familyName) { + Text("enter your last name") + } + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt new file mode 100644 index 000000000..5383b3147 --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt @@ -0,0 +1,27 @@ +package edu.stanford.spezi.core.design.personalInfo.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.component.ImageResource +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.UserProfileComposable + +@Composable +fun UserProfileTestComposable() { + Column { + UserProfileComposable( + Modifier.height(100.dp), + PersonNameComponents(givenName = "Paul", familyName = "Schmiedmayer") + ) + UserProfileComposable( + Modifier.height(200.dp), + PersonNameComponents(givenName = "Leland", familyName = "Stanford"), + ImageResource.Vector(Icons.Default.Person) + ) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/NameFieldsTestSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/NameFieldsTestSimulator.kt new file mode 100644 index 000000000..c71708838 --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/NameFieldsTestSimulator.kt @@ -0,0 +1,22 @@ +package edu.stanford.spezi.core.design.personalInfo.simulators + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput + +class NameFieldsTestSimulator( + private val composeTestRule: ComposeTestRule, +) { + + fun assertTextExists(text: String) { + composeTestRule + .onNodeWithText(text) + .assertExists() + } + + fun enterText(placeholder: String, text: String) { + composeTestRule + .onNodeWithText(placeholder) + .performTextInput(text) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt new file mode 100644 index 000000000..5c5d0133c --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt @@ -0,0 +1,27 @@ +package edu.stanford.spezi.core.design.personalInfo.simulators + +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithContentDescription +import androidx.compose.ui.test.onNodeWithText + +class UserProfileTestSimulator( + private val composeTestRule: ComposeTestRule, +) { + + fun assertUserInitialsExists(exists: Boolean, text: String) { + val node = composeTestRule + .onNodeWithText(text) + if (exists) { + node.assertExists() + } else { + node.assertDoesNotExist() + } + } + + fun assertImageExists() { + composeTestRule + .onAllNodesWithContentDescription("") + .assertCountEquals(1) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/DefaultValidationRulesSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/DefaultValidationRulesSimulator.kt deleted file mode 100644 index 36415d43d..000000000 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/DefaultValidationRulesSimulator.kt +++ /dev/null @@ -1,9 +0,0 @@ -package edu.stanford.spezi.core.design.simulator - -import androidx.compose.ui.test.junit4.ComposeTestRule - -class DefaultValidationRulesSimulator( - private val composeTestRule: ComposeTestRule, -) { - -} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/ValidationTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/ValidationTest.kt similarity index 66% rename from core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/ValidationTest.kt rename to core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/ValidationTest.kt index 11e20aa2c..8f3ab5b26 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/ValidationTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/ValidationTest.kt @@ -1,12 +1,8 @@ -package edu.stanford.spezi.core.design +package edu.stanford.spezi.core.design.views -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import edu.stanford.spezi.core.design.composables.FocusValidationRules -import edu.stanford.spezi.core.design.simulator.FocusValidationRulesSimulator -import edu.stanford.spezi.core.design.views.validation.state.ValidationContext -import kotlinx.coroutines.delay +import edu.stanford.spezi.core.design.views.composables.FocusValidationRules +import edu.stanford.spezi.core.design.views.simulators.FocusValidationRulesSimulator import org.junit.Before import org.junit.Rule import org.junit.Test @@ -33,12 +29,11 @@ class ValidationTest { clickValidateButton() assertLastState(false) assertPasswordMessageExists(true) - // assertEmptyMessageExists(true) + assertEmptyMessageExists(true) } } private fun focusValidationRules(block: FocusValidationRulesSimulator.() -> Unit) { FocusValidationRulesSimulator(composeTestRule).apply(block) } - } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/DefaultValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/DefaultValidationRules.kt similarity index 96% rename from core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/DefaultValidationRules.kt rename to core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/DefaultValidationRules.kt index 1f57e3b28..8599fcd84 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/DefaultValidationRules.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/DefaultValidationRules.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.composables +package edu.stanford.spezi.core.design.views.composables import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/FocusValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/FocusValidationRules.kt similarity index 96% rename from core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/FocusValidationRules.kt rename to core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/FocusValidationRules.kt index 57acecb0e..276a82651 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/composables/FocusValidationRules.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/FocusValidationRules.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.composables +package edu.stanford.spezi.core.design.views.composables import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -8,7 +8,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.validation.Validate import edu.stanford.spezi.core.design.views.validation.ValidationRule diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/FocusValidationRulesSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/FocusValidationRulesSimulator.kt similarity index 92% rename from core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/FocusValidationRulesSimulator.kt rename to core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/FocusValidationRulesSimulator.kt index 78231742d..7f35f12f3 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/simulator/FocusValidationRulesSimulator.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/FocusValidationRulesSimulator.kt @@ -1,8 +1,6 @@ -package edu.stanford.spezi.core.design.simulator +package edu.stanford.spezi.core.design.views.simulators -import androidx.compose.material3.Text import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt index b7da19e68..4ddab4c19 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -28,19 +27,14 @@ import kotlin.math.min fun UserProfileComposable( modifier: Modifier = Modifier, name: PersonNameComponents, - imageLoader: suspend () -> ImageResource? = { null }, + image: ImageResource? = null, ) { - val image = remember { mutableStateOf(null) } val size = remember { mutableStateOf(IntSize.Zero) } - LaunchedEffect(Unit) { - image.value = imageLoader() - } - Box(modifier.onSizeChanged { size.value = it }.aspectRatio(1f)) { val sideLength = min(size.value.height, size.value.width).dp Box(modifier.size(sideLength, sideLength), contentAlignment = Alignment.Center) { - image.value?.let { + image?.let { ImageResourceComposable( it, "", // TODO: Add contentDescription to ImageResource directly? diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt index 706ac77b5..a4f61f9f1 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt @@ -9,7 +9,7 @@ data class ValidationRule internal constructor( val id: UUID = UUID(), val rule: (String) -> Boolean, val message: StringResource, - val effect: CascadingValidationEffect = CascadingValidationEffect.CONTINUE + val effect: CascadingValidationEffect = CascadingValidationEffect.CONTINUE, ) { companion object { operator fun invoke(regex: Regex, message: StringResource): ValidationRule = diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt index 53896bc3d..50d5e1583 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt @@ -5,11 +5,11 @@ import androidx.compose.runtime.compositionLocalOf internal val LocalCapturedValidationStateEntries = compositionLocalOf { CapturedValidationStateEntries() } internal data class CapturedValidationStateEntries( - private var _entries: MutableList = mutableListOf() + private var mutableEntries: MutableList = mutableListOf(), ) { - val entries: List get() = _entries + val entries: List get() = mutableEntries fun add(state: CapturedValidationState) { - _entries.add(state) + mutableEntries.add(state) } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt index 50ec874b8..f46992f52 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt @@ -6,7 +6,7 @@ import java.util.UUID data class FailedValidationResult( val id: UUID, - val message: StringResource + val message: StringResource, ) { companion object { operator fun invoke(rule: ValidationRule) = diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt index bcc392366..a12a71d86 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt @@ -1,7 +1,7 @@ package edu.stanford.spezi.core.design.views.validation.state data class ValidationContext internal constructor( - private val entries: List = emptyList() + private val entries: List = emptyList(), ) : Iterable { val allInputValid: Boolean get() = entries.all { it.inputValid } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt index 8be8a126a..b07aabf20 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt @@ -10,7 +10,7 @@ import edu.stanford.spezi.core.design.views.validation.state.FailedValidationRes @Composable fun ValidationResultsComposable( - results: List + results: List, ) { Column( horizontalAlignment = Alignment.Start @@ -23,4 +23,4 @@ fun ValidationResultsComposable( ) } } -} \ No newline at end of file +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index 985151613..6177b04be 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -25,7 +25,7 @@ fun VerifiableTextField( text: MutableState, type: TextFieldType = TextFieldType.TEXT, disableAutocorrection: Boolean = false, - footer: @Composable () -> Unit = {} + footer: @Composable () -> Unit = {}, ) { VerifiableTextField( text, @@ -42,7 +42,7 @@ fun VerifiableTextField( type: TextFieldType = TextFieldType.TEXT, disableAutocorrection: Boolean = false, label: @Composable () -> Unit, - footer: @Composable () -> Unit = {} + footer: @Composable () -> Unit = {}, ) { val validationEngine = LocalValidationEngine.current diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt index 5d5ee58d9..cb2eb4abb 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt @@ -8,7 +8,7 @@ import edu.stanford.spezi.core.design.views.views.model.OperationState @Composable fun OperationStateAlert( - state: MutableState + state: MutableState, ) { val viewState = remember { mutableStateOf(state.value.representation) } MapOperationStateToViewState(state.value, viewState) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt index 916d9cf6c..b6cf3357b 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt @@ -25,7 +25,7 @@ fun ProcessingOverlay( isProcessing: Boolean, processingContent: @Composable () -> Unit = { CircularProgressIndicator() }, content: @Composable () -> Unit, - ) { +) { Box { Box(Modifier.alpha(if (isProcessing) 0f else 1f)) { content() diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt index 690099959..7efbf01a3 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt @@ -1,7 +1,6 @@ package edu.stanford.spezi.core.design.views.views.views.button import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -9,7 +8,6 @@ import androidx.compose.runtime.rememberCoroutineScope import edu.stanford.spezi.core.design.component.Button import edu.stanford.spezi.core.design.views.views.compositionLocal.LocalProcessingDebounceDuration import edu.stanford.spezi.core.design.views.views.model.ViewState -import edu.stanford.spezi.core.utils.UUID import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -23,7 +21,7 @@ private enum class SuspendButtonState { fun SuspendButton( state: MutableState, action: suspend () -> Unit, - label: @Composable () -> Unit + label: @Composable () -> Unit, ) { val buttonState = remember { mutableStateOf(SuspendButtonState.IDLE) } val coroutineScope = rememberCoroutineScope() diff --git a/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt b/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt index 2c78297f4..fd7c4203d 100644 --- a/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt +++ b/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt @@ -1,17 +1,16 @@ package edu.stanford.spezi.core.design import com.google.common.truth.Truth.assertThat -import edu.stanford.spezi.core.design.views.validation.ValidationEngine +import edu.stanford.spezi.core.design.views.validation.ValidationEngineImpl import edu.stanford.spezi.core.design.views.validation.ValidationRule import edu.stanford.spezi.core.design.views.validation.nonEmpty -import kotlinx.coroutines.CoroutineScope import org.junit.Test class SpeziValidationTest { @Test fun testValidationDebounce() { - val engine = ValidationEngine(rules = listOf(ValidationRule.nonEmpty)) + val engine = ValidationEngineImpl(rules = listOf(ValidationRule.nonEmpty)) engine.submit("Valid") assertThat(engine.inputValid).isTrue() @@ -30,5 +29,4 @@ class SpeziValidationTest { assertThat(engine.inputValid).isTrue() // valid state is reported instantly assertThat(engine.validationResults).isEmpty() } - } From f3f516b9239bf33313d2c8f9a2473c774044f581 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 13:43:22 -0800 Subject: [PATCH 07/51] Finish up SpeziViews --- .../design/personalInfo/UserProfileTest.kt | 3 +- .../composables/NameFieldsTestComposable.kt | 4 +- .../composables/UserProfileTestComposable.kt | 14 ++-- .../simulators/UserProfileTestSimulator.kt | 8 ++ .../{views => validation}/ValidationTest.kt | 6 +- .../composables/DefaultValidationRules.kt | 38 +++++++++ .../composables/FocusValidationRules.kt | 16 ++-- .../FocusValidationRulesSimulator.kt | 2 +- .../spezi/core/design/views/MarkdownTest.kt | 42 ++++++++++ .../core/design/views/SuspendButtonTest.kt | 39 +++++++++ .../composables/DefaultValidationRules.kt | 38 --------- .../composables/MarkdownTestComposable.kt | 24 ++++++ .../SuspendButtonTestComposable.kt | 46 +++++++++++ .../views/simulators/MarkdownTestSimulator.kt | 25 ++++++ .../simulators/SuspendButtonTestSimulator.kt | 70 ++++++++++++++++ .../component/markdown/MarkdownElement.kt | 10 +-- .../personalInfo/PersonNameComponents.kt | 2 +- .../personalInfo/UserProfileComposable.kt | 29 +++++-- .../personalInfo/fields/NameFieldRow.kt | 6 +- .../personalInfo/fields/NameTextField.kt | 4 +- .../validation/CascadingValidationEffect.kt | 2 +- .../validation/ValidationEngine.kt | 6 +- .../validation/ValidationModifier.kt | 35 ++++---- .../validation/ValidationRule.kt | 4 +- .../validation/ValidationRuleDefaults.kt | 2 +- .../ValidationDebounceDuration.kt | 2 +- .../configuration/ValidationEngine.kt | 6 ++ .../ValidationEngineConfiguration.kt | 9 ++ .../state/CapturedValidationState.kt | 4 +- .../state/CapturedValidationStateEntries.kt | 2 +- .../state/FailedValidationResult.kt | 4 +- .../validation/state/ReceiveValidation.kt | 2 +- .../validation/state/ValidationContext.kt | 2 +- .../views/ValidationResultsComposable.kt | 4 +- .../validation/views/VerifiableTextField.kt | 4 +- .../ProcessingDebounceDuration.kt | 2 +- .../views/layout/DescriptionGridRow.kt | 2 +- .../views/model/OperationState.kt | 2 +- .../views/model/ViewState.kt | 2 +- .../viewState/OperationStateAlert.kt | 4 +- .../viewModifier/viewState/ViewStateAlert.kt | 7 +- .../viewModifier/viewState/ViewStateMapper.kt | 6 +- .../views/views/button/ProcessingOverlay.kt | 17 +++- .../views/views/button/SuspendButton.kt | 25 ++++-- .../validation/views/views/text/Markdown.kt | 82 +++++++++++++++++++ .../configuration/ValidationEngine.kt | 6 -- .../ValidationEngineConfiguration.kt | 9 -- .../spezi/core/design/SpeziValidationTest.kt | 6 +- .../simulator/ContactComposableSimulator.kt | 2 +- 49 files changed, 533 insertions(+), 153 deletions(-) rename core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/{views => validation}/ValidationTest.kt (80%) create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/DefaultValidationRules.kt rename core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/{views => validation}/composables/FocusValidationRules.kt (77%) rename core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/{views => validation}/simulators/FocusValidationRulesSimulator.kt (96%) create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/MarkdownTest.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/SuspendButtonTest.kt delete mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/DefaultValidationRules.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/MarkdownTestComposable.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/MarkdownTestSimulator.kt create mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/SuspendButtonTestSimulator.kt rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/personalInfo/PersonNameComponents.kt (94%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/personalInfo/UserProfileComposable.kt (67%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/personalInfo/fields/NameFieldRow.kt (89%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/personalInfo/fields/NameTextField.kt (92%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/CascadingValidationEffect.kt (51%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/ValidationEngine.kt (93%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/ValidationModifier.kt (58%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/ValidationRule.kt (88%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/ValidationRuleDefaults.kt (96%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/configuration/ValidationDebounceDuration.kt (77%) create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngine.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngineConfiguration.kt rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/state/CapturedValidationState.kt (83%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/state/CapturedValidationStateEntries.kt (87%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/state/FailedValidationResult.kt (75%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/state/ReceiveValidation.kt (91%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/state/ValidationContext.kt (95%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/views/ValidationResultsComposable.kt (80%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/validation/views/VerifiableTextField.kt (94%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/compositionLocal/ProcessingDebounceDuration.kt (71%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/layout/DescriptionGridRow.kt (96%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/model/OperationState.kt (50%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/model/ViewState.kt (90%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/viewModifier/viewState/OperationStateAlert.kt (73%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/viewModifier/viewState/ViewStateAlert.kt (76%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/viewModifier/viewState/ViewStateMapper.kt (60%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/views/button/ProcessingOverlay.kt (60%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{views => validation}/views/views/button/SuspendButton.kt (76%) create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/text/Markdown.kt delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt index 7385cfd58..c36d75fcf 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt @@ -23,7 +23,8 @@ class UserProfileTest { fun testUserProfile() { userProfile { assertUserInitialsExists(true, "PS") - assertUserInitialsExists(false, "LS") + assertUserInitialsExists(true, "LS") + waitUntilUserInitialsDisappear("LS") assertImageExists() } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt index 9bcf547bb..81b1d9fb6 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt @@ -7,8 +7,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents -import edu.stanford.spezi.core.design.views.personalInfo.fields.NameFieldRow +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.validation.personalInfo.fields.NameFieldRow @Composable fun NameFieldsTestComposable() { diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt index 5383b3147..a86bd1132 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt @@ -8,8 +8,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import edu.stanford.spezi.core.design.component.ImageResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents -import edu.stanford.spezi.core.design.views.personalInfo.UserProfileComposable +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.validation.personalInfo.UserProfileComposable +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.seconds @Composable fun UserProfileTestComposable() { @@ -20,8 +22,10 @@ fun UserProfileTestComposable() { ) UserProfileComposable( Modifier.height(200.dp), - PersonNameComponents(givenName = "Leland", familyName = "Stanford"), - ImageResource.Vector(Icons.Default.Person) - ) + PersonNameComponents(givenName = "Leland", familyName = "Stanford") + ) { + delay(0.5.seconds) + return@UserProfileComposable ImageResource.Vector(Icons.Default.Person) + } } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt index 5c5d0133c..552d409df 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt @@ -3,6 +3,7 @@ package edu.stanford.spezi.core.design.personalInfo.simulators import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onAllNodesWithContentDescription +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithText class UserProfileTestSimulator( @@ -19,6 +20,13 @@ class UserProfileTestSimulator( } } + fun waitUntilUserInitialsDisappear(text: String, timeoutMillis: Long = 1_000) { + composeTestRule.waitUntil(timeoutMillis = timeoutMillis) { + composeTestRule.onAllNodesWithText(text) + .fetchSemanticsNodes().isEmpty() + } + } + fun assertImageExists() { composeTestRule .onAllNodesWithContentDescription("") diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/ValidationTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/ValidationTest.kt similarity index 80% rename from core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/ValidationTest.kt rename to core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/ValidationTest.kt index 8f3ab5b26..340a5d733 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/ValidationTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/ValidationTest.kt @@ -1,8 +1,8 @@ -package edu.stanford.spezi.core.design.views +package edu.stanford.spezi.core.design.validation import androidx.compose.ui.test.junit4.createComposeRule -import edu.stanford.spezi.core.design.views.composables.FocusValidationRules -import edu.stanford.spezi.core.design.views.simulators.FocusValidationRulesSimulator +import edu.stanford.spezi.core.design.validation.composables.FocusValidationRules +import edu.stanford.spezi.core.design.validation.simulators.FocusValidationRulesSimulator import org.junit.Before import org.junit.Rule import org.junit.Test diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/DefaultValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/DefaultValidationRules.kt new file mode 100644 index 000000000..55b0bb61c --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/DefaultValidationRules.kt @@ -0,0 +1,38 @@ +package edu.stanford.spezi.core.design.validation.composables + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.validation.validation.Validate +import edu.stanford.spezi.core.design.validation.validation.ValidationRule +import edu.stanford.spezi.core.design.validation.validation.asciiLettersOnly +import edu.stanford.spezi.core.design.validation.validation.mediumPassword +import edu.stanford.spezi.core.design.validation.validation.minimalEmail +import edu.stanford.spezi.core.design.validation.validation.minimalPassword +import edu.stanford.spezi.core.design.validation.validation.nonEmpty +import edu.stanford.spezi.core.design.validation.validation.strongPassword +import edu.stanford.spezi.core.design.validation.validation.unicodeLettersOnly +import edu.stanford.spezi.core.design.validation.validation.views.VerifiableTextField + +@Composable +fun DefaultValidationRules() { + val input = remember { mutableStateOf("") } + val rules = remember { + listOf( + ValidationRule.nonEmpty, + ValidationRule.unicodeLettersOnly, + ValidationRule.asciiLettersOnly, + ValidationRule.minimalEmail, + ValidationRule.minimalPassword, + ValidationRule.mediumPassword, + ValidationRule.strongPassword + ) + } + Validate(input.value, rules) { + VerifiableTextField( + StringResource("Field"), + text = input + ) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/FocusValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt similarity index 77% rename from core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/FocusValidationRules.kt rename to core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt index 276a82651..e7a55a163 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/FocusValidationRules.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.composables +package edu.stanford.spezi.core.design.validation.composables import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -9,13 +9,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.validation.Validate -import edu.stanford.spezi.core.design.views.validation.ValidationRule -import edu.stanford.spezi.core.design.views.validation.minimalPassword -import edu.stanford.spezi.core.design.views.validation.nonEmpty -import edu.stanford.spezi.core.design.views.validation.state.ReceiveValidation -import edu.stanford.spezi.core.design.views.validation.state.ValidationContext -import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField +import edu.stanford.spezi.core.design.validation.validation.Validate +import edu.stanford.spezi.core.design.validation.validation.ValidationRule +import edu.stanford.spezi.core.design.validation.validation.minimalPassword +import edu.stanford.spezi.core.design.validation.validation.nonEmpty +import edu.stanford.spezi.core.design.validation.validation.state.ReceiveValidation +import edu.stanford.spezi.core.design.validation.validation.state.ValidationContext +import edu.stanford.spezi.core.design.validation.validation.views.VerifiableTextField enum class Field { INPUT, NON_EMPTY_INPUT diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/FocusValidationRulesSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/simulators/FocusValidationRulesSimulator.kt similarity index 96% rename from core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/FocusValidationRulesSimulator.kt rename to core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/simulators/FocusValidationRulesSimulator.kt index 7f35f12f3..62d828e16 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/FocusValidationRulesSimulator.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/simulators/FocusValidationRulesSimulator.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.simulators +package edu.stanford.spezi.core.design.validation.simulators import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.ComposeTestRule diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/MarkdownTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/MarkdownTest.kt new file mode 100644 index 000000000..56a568be3 --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/MarkdownTest.kt @@ -0,0 +1,42 @@ +package edu.stanford.spezi.core.design.views + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.core.design.views.composables.MarkdownTestComposable +import edu.stanford.spezi.core.design.views.simulators.MarkdownTestSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class MarkdownTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun init() { + composeTestRule.setContent { + MarkdownTestComposable() + } + } + + @Test + fun testMarkdown() { + markdown { + waitForTextToAppear( + "This is a markdown example.", + timeoutMillis = 100 + ) + assertTextExists( + "This is a markdown example taking half a second to load.", + exists = false + ) + waitForTextToAppear( + "This is a markdown example taking half a second to load.", + ) + } + } + + private fun markdown(block: MarkdownTestSimulator.() -> Unit) { + MarkdownTestSimulator(composeTestRule).apply(block) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/SuspendButtonTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/SuspendButtonTest.kt new file mode 100644 index 000000000..f5df1198a --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/SuspendButtonTest.kt @@ -0,0 +1,39 @@ +package edu.stanford.spezi.core.design.views + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.core.design.views.composables.SuspendButtonTestComposable +import edu.stanford.spezi.core.design.views.simulators.SuspendButtonTestSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class SuspendButtonTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun init() { + composeTestRule.setContent { + SuspendButtonTestComposable() + } + } + + @Test + fun testSuspendButton() { + suspendButton { + clickHelloWorldButton() + waitForHelloWorldButtonAction() + resetHelloWorldButtonAction() + + clickHelloThrowingWorldButton() + assertViewStateAlertAppeared("Error was thrown!") + dismissViewStateAlert() + assertHelloThrowingWorldButtonIsEnabled() + } + } + + private fun suspendButton(block: SuspendButtonTestSimulator.() -> Unit) { + SuspendButtonTestSimulator(composeTestRule).apply(block) + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/DefaultValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/DefaultValidationRules.kt deleted file mode 100644 index 8599fcd84..000000000 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/DefaultValidationRules.kt +++ /dev/null @@ -1,38 +0,0 @@ -package edu.stanford.spezi.core.design.views.composables - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.validation.Validate -import edu.stanford.spezi.core.design.views.validation.ValidationRule -import edu.stanford.spezi.core.design.views.validation.asciiLettersOnly -import edu.stanford.spezi.core.design.views.validation.mediumPassword -import edu.stanford.spezi.core.design.views.validation.minimalEmail -import edu.stanford.spezi.core.design.views.validation.minimalPassword -import edu.stanford.spezi.core.design.views.validation.nonEmpty -import edu.stanford.spezi.core.design.views.validation.strongPassword -import edu.stanford.spezi.core.design.views.validation.unicodeLettersOnly -import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField - -@Composable -fun DefaultValidationRules() { - val input = remember { mutableStateOf("") } - val rules = remember { - listOf( - ValidationRule.nonEmpty, - ValidationRule.unicodeLettersOnly, - ValidationRule.asciiLettersOnly, - ValidationRule.minimalEmail, - ValidationRule.minimalPassword, - ValidationRule.mediumPassword, - ValidationRule.strongPassword - ) - } - Validate(input.value, rules) { - VerifiableTextField( - StringResource("Field"), - text = input - ) - } -} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/MarkdownTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/MarkdownTestComposable.kt new file mode 100644 index 000000000..d223778c2 --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/MarkdownTestComposable.kt @@ -0,0 +1,24 @@ +package edu.stanford.spezi.core.design.views.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import edu.stanford.spezi.core.design.validation.views.views.text.MarkdownBytes +import edu.stanford.spezi.core.design.validation.views.views.text.MarkdownString +import kotlinx.coroutines.delay +import java.nio.charset.StandardCharsets +import kotlin.time.Duration.Companion.milliseconds + +@Composable +fun MarkdownTestComposable() { + Column { + MarkdownBytes( + bytes = { + delay(500.milliseconds) + "This is a markdown **example** taking half a second to load." + .toByteArray(StandardCharsets.UTF_8) + } + ) + + MarkdownString("This is a markdown **example**.") + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt new file mode 100644 index 000000000..13af8d14d --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt @@ -0,0 +1,46 @@ +package edu.stanford.spezi.core.design.views.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.validation.views.model.ViewState +import edu.stanford.spezi.core.design.validation.views.viewModifier.viewState.ViewStateAlert +import edu.stanford.spezi.core.design.validation.views.views.button.SuspendButton +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds + +class CustomError : Throwable() { + override val message = "Error was thrown!" +} + +@Composable +fun SuspendButtonTestComposable() { + var showCompleted by remember { mutableStateOf(false) } + val viewState = remember { mutableStateOf(ViewState.Idle) } + + ViewStateAlert(viewState) + + Column { + if (showCompleted) { + Text("Action executed") + Button(onClick = { showCompleted = false }) { + Text("Reset") + } + } else { + SuspendButton(StringResource("Hello World")) { + delay(500.milliseconds) + showCompleted = true + } + + SuspendButton(StringResource("Hello Throwing World"), viewState) { + throw CustomError() + } + } + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/MarkdownTestSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/MarkdownTestSimulator.kt new file mode 100644 index 000000000..8acbf488f --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/MarkdownTestSimulator.kt @@ -0,0 +1,25 @@ +package edu.stanford.spezi.core.design.views.simulators + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText + +class MarkdownTestSimulator( + private val composeTestRule: ComposeTestRule, +) { + fun assertTextExists(text: String, exists: Boolean = true) { + val node = composeTestRule.onNodeWithText(text) + if (exists) { + node.assertExists() + } else { + node.assertDoesNotExist() + } + } + + fun waitForTextToAppear(text: String, timeoutMillis: Long = 1_000) { + composeTestRule.waitUntil(timeoutMillis) { + composeTestRule.onAllNodesWithText(text) + .fetchSemanticsNodes().isNotEmpty() + } + } +} diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/SuspendButtonTestSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/SuspendButtonTestSimulator.kt new file mode 100644 index 000000000..8542f64f4 --- /dev/null +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/simulators/SuspendButtonTestSimulator.kt @@ -0,0 +1,70 @@ +package edu.stanford.spezi.core.design.views.simulators + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class SuspendButtonTestSimulator( + private val composeTestRule: ComposeTestRule, +) { + + fun clickHelloWorldButton() { + composeTestRule + .onNodeWithText("Hello World") + .assertHasClickAction() + .performClick() + } + + fun waitForHelloWorldButtonAction() { + composeTestRule.waitUntil { + composeTestRule.onAllNodesWithText("Action executed") + .fetchSemanticsNodes().size == 1 + } + + composeTestRule + .onNodeWithText("Action executed") + .assertExists() + } + + fun resetHelloWorldButtonAction() { + composeTestRule + .onNodeWithText("Reset") + .assertHasClickAction() + .performClick() + } + + fun clickHelloThrowingWorldButton() { + composeTestRule + .onNodeWithText("Hello Throwing World") + .assertHasClickAction() + .assertIsEnabled() + .performClick() + } + + fun assertViewStateAlertAppeared(message: String) { + composeTestRule + .onNodeWithText("Error") + .assertExists() + + composeTestRule + .onNodeWithText(message) + .assertExists() + } + + fun dismissViewStateAlert() { + composeTestRule + .onNodeWithText("OK") + .assertHasClickAction() + .performClick() + } + + fun assertHelloThrowingWorldButtonIsEnabled() { + composeTestRule + .onNodeWithText("Hello Throwing World") + .assertHasClickAction() + .assertIsEnabled() + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownElement.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownElement.kt index e8e8d9f4f..f88d0b038 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownElement.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownElement.kt @@ -1,8 +1,8 @@ package edu.stanford.spezi.core.design.component.markdown -sealed class MarkdownElement { - data class Heading(val level: Int, val text: String) : MarkdownElement() - data class Paragraph(val text: String) : MarkdownElement() - data class Bold(val text: String) : MarkdownElement() - data class ListItem(val text: String) : MarkdownElement() +sealed interface MarkdownElement { + data class Heading(val level: Int, val text: String) : MarkdownElement + data class Paragraph(val text: String) : MarkdownElement + data class Bold(val text: String) : MarkdownElement + data class ListItem(val text: String) : MarkdownElement } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/PersonNameComponents.kt similarity index 94% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/PersonNameComponents.kt index 471037d08..97406c988 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/PersonNameComponents.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.personalInfo +package edu.stanford.spezi.core.design.validation.personalInfo data class PersonNameComponents( var namePrefix: String? = null, diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/UserProfileComposable.kt similarity index 67% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/UserProfileComposable.kt index 4ddab4c19..77e8db0d2 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/UserProfileComposable.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.personalInfo +package edu.stanford.spezi.core.design.validation.personalInfo import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -8,8 +8,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue 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.draw.clip @@ -21,20 +24,32 @@ import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.ImageResourceComposable import edu.stanford.spezi.core.design.theme.Colors import edu.stanford.spezi.core.design.theme.lighten +import edu.stanford.spezi.core.logging.SpeziLogger import kotlin.math.min @Composable fun UserProfileComposable( modifier: Modifier = Modifier, name: PersonNameComponents, - image: ImageResource? = null, + imageLoader: suspend () -> ImageResource? = { null }, ) { - val size = remember { mutableStateOf(IntSize.Zero) } + var size by remember { mutableStateOf(IntSize.Zero) } + var loadedImage by remember { mutableStateOf(null) } - Box(modifier.onSizeChanged { size.value = it }.aspectRatio(1f)) { - val sideLength = min(size.value.height, size.value.width).dp + LaunchedEffect(Unit) { + loadedImage = runCatching { imageLoader() } + .onFailure { SpeziLogger.e(it) { "Failed to load image" } } + .getOrNull() + } + + val formattedName = remember(name) { + name.formatted(PersonNameComponents.FormatStyle.ABBREVIATED) + } + + Box(modifier.onSizeChanged { size = it }.aspectRatio(1f)) { + val sideLength = min(size.height, size.width).dp Box(modifier.size(sideLength, sideLength), contentAlignment = Alignment.Center) { - image?.let { + loadedImage?.let { ImageResourceComposable( it, "", // TODO: Add contentDescription to ImageResource directly? @@ -45,7 +60,7 @@ fun UserProfileComposable( } ?: run { Box(Modifier.background(Colors.secondary, CircleShape).fillMaxSize(), contentAlignment = Alignment.Center) { Text( - name.formatted(PersonNameComponents.FormatStyle.ABBREVIATED), + formattedName, fontSize = (sideLength.value * 0.2).sp, color = Colors.secondary.lighten(), ) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameFieldRow.kt similarity index 89% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameFieldRow.kt index 48102fe1f..3c4f5e046 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameFieldRow.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.personalInfo.fields +package edu.stanford.spezi.core.design.validation.personalInfo.fields import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -12,8 +12,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents -import edu.stanford.spezi.core.design.views.views.layout.DescriptionGridRow +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.validation.views.layout.DescriptionGridRow import kotlin.reflect.KMutableProperty1 @Composable diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameTextField.kt similarity index 92% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameTextField.kt index 5ffacc1cc..2b0528625 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameTextField.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.personalInfo.fields +package edu.stanford.spezi.core.design.validation.personalInfo.fields import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text @@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents import kotlin.reflect.KMutableProperty1 @Composable diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/CascadingValidationEffect.kt similarity index 51% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/CascadingValidationEffect.kt index e1676ff23..75ba0a587 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/CascadingValidationEffect.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation +package edu.stanford.spezi.core.design.validation.validation enum class CascadingValidationEffect { CONTINUE, INTERCEPT diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationEngine.kt similarity index 93% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationEngine.kt index 42f50e87d..39f333125 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationEngine.kt @@ -1,8 +1,8 @@ -package edu.stanford.spezi.core.design.views.validation +package edu.stanford.spezi.core.design.validation.validation import androidx.compose.runtime.mutableStateOf -import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION -import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult +import edu.stanford.spezi.core.design.validation.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION +import edu.stanford.spezi.core.design.validation.validation.state.FailedValidationResult import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationModifier.kt similarity index 58% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationModifier.kt index 02a87f526..7c7019f60 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationModifier.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation +package edu.stanford.spezi.core.design.validation.validation import android.annotation.SuppressLint import androidx.compose.runtime.Composable @@ -7,12 +7,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationDebounce -import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine -import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngineConfiguration -import edu.stanford.spezi.core.design.views.validation.state.CapturedValidationState -import edu.stanford.spezi.core.design.views.validation.state.LocalCapturedValidationStateEntries -import kotlin.time.Duration +import edu.stanford.spezi.core.design.validation.validation.configuration.LocalValidationDebounce +import edu.stanford.spezi.core.design.validation.validation.configuration.LocalValidationEngine +import edu.stanford.spezi.core.design.validation.validation.configuration.LocalValidationEngineConfiguration +import edu.stanford.spezi.core.design.validation.validation.state.CapturedValidationState +import edu.stanford.spezi.core.design.validation.validation.state.LocalCapturedValidationStateEntries @Composable fun Validate( @@ -20,10 +19,12 @@ fun Validate( message: StringResource, content: @Composable () -> Unit, ) { - val rule = ValidationRule( - rule = { it.isEmpty() }, - message = message - ) + val rule = remember { + ValidationRule( + rule = { it.isEmpty() }, + message = message + ) + } Validate( input = if (predicate) "" else "FALSE", rules = listOf(rule), @@ -39,10 +40,14 @@ fun Validate( content: @Composable () -> Unit, ) { val validationDebounce = LocalValidationDebounce.current - val previousValidationDebounce = remember { mutableStateOf(null) } val validationEngineConfiguration = LocalValidationEngineConfiguration.current - val previousValidationEngineConfiguration = remember { mutableStateOf(null) } - val engine = remember { ValidationEngineImpl(rules, validationDebounce, validationEngineConfiguration) } + val engine = remember { + ValidationEngineImpl( + rules, + validationDebounce, + validationEngineConfiguration + ) + } LaunchedEffect(input) { engine.submit(input, debounce = true) @@ -50,12 +55,10 @@ fun Validate( LaunchedEffect(validationDebounce) { engine.debounceDuration = validationDebounce - previousValidationDebounce.value = validationDebounce } LaunchedEffect(validationEngineConfiguration) { engine.configuration = validationEngineConfiguration - previousValidationEngineConfiguration.value = validationEngineConfiguration } val hasFocus = remember { mutableStateOf(false) } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRule.kt similarity index 88% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRule.kt index a4f61f9f1..e488f4da3 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRule.kt @@ -1,7 +1,7 @@ -package edu.stanford.spezi.core.design.views.validation +package edu.stanford.spezi.core.design.validation.validation import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult +import edu.stanford.spezi.core.design.validation.validation.state.FailedValidationResult import edu.stanford.spezi.core.utils.UUID import java.util.UUID diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRuleDefaults.kt similarity index 96% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRuleDefaults.kt index a6aa86bc7..791d4c37f 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRuleDefaults.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation +package edu.stanford.spezi.core.design.validation.validation import edu.stanford.spezi.core.design.component.StringResource import java.nio.charset.StandardCharsets diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationDebounceDuration.kt similarity index 77% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationDebounceDuration.kt index 15cef5d76..000da7721 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationDebounceDuration.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation.configuration +package edu.stanford.spezi.core.design.validation.validation.configuration import androidx.compose.runtime.compositionLocalOf import kotlin.time.Duration.Companion.seconds diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngine.kt new file mode 100644 index 000000000..4ba7077af --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngine.kt @@ -0,0 +1,6 @@ +package edu.stanford.spezi.core.design.validation.validation.configuration + +import androidx.compose.runtime.compositionLocalOf +import edu.stanford.spezi.core.design.validation.validation.ValidationEngine + +val LocalValidationEngine = compositionLocalOf { null } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngineConfiguration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngineConfiguration.kt new file mode 100644 index 000000000..117c756e5 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngineConfiguration.kt @@ -0,0 +1,9 @@ +package edu.stanford.spezi.core.design.validation.validation.configuration + +import androidx.compose.runtime.compositionLocalOf +import edu.stanford.spezi.core.design.validation.validation.ValidationEngine +import edu.stanford.spezi.core.design.validation.validation.ValidationEngineConfiguration + +val LocalValidationEngineConfiguration = compositionLocalOf { + ValidationEngineConfiguration.noneOf(ValidationEngine.ConfigurationOption::class.java) +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationState.kt similarity index 83% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationState.kt index 5288bb5f7..c04602159 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationState.kt @@ -1,7 +1,7 @@ -package edu.stanford.spezi.core.design.views.validation.state +package edu.stanford.spezi.core.design.validation.validation.state import androidx.compose.runtime.MutableState -import edu.stanford.spezi.core.design.views.validation.ValidationEngine +import edu.stanford.spezi.core.design.validation.validation.ValidationEngine data class CapturedValidationState internal constructor( private val engine: ValidationEngine, diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationStateEntries.kt similarity index 87% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationStateEntries.kt index 50d5e1583..949a771b3 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationStateEntries.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation.state +package edu.stanford.spezi.core.design.validation.validation.state import androidx.compose.runtime.compositionLocalOf diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/FailedValidationResult.kt similarity index 75% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/FailedValidationResult.kt index f46992f52..35bc2b51c 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/FailedValidationResult.kt @@ -1,7 +1,7 @@ -package edu.stanford.spezi.core.design.views.validation.state +package edu.stanford.spezi.core.design.validation.validation.state import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.validation.ValidationRule +import edu.stanford.spezi.core.design.validation.validation.ValidationRule import java.util.UUID data class FailedValidationResult( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ReceiveValidation.kt similarity index 91% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ReceiveValidation.kt index 94ce4e850..bddafd645 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ReceiveValidation.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation.state +package edu.stanford.spezi.core.design.validation.validation.state import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ValidationContext.kt similarity index 95% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ValidationContext.kt index a12a71d86..18873e6a8 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ValidationContext.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation.state +package edu.stanford.spezi.core.design.validation.validation.state data class ValidationContext internal constructor( private val entries: List = emptyList(), diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/ValidationResultsComposable.kt similarity index 80% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/ValidationResultsComposable.kt index b07aabf20..29fb9bb0b 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/ValidationResultsComposable.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation.views +package edu.stanford.spezi.core.design.validation.validation.views import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import edu.stanford.spezi.core.design.theme.TextStyles -import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult +import edu.stanford.spezi.core.design.validation.validation.state.FailedValidationResult @Composable fun ValidationResultsComposable( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/VerifiableTextField.kt similarity index 94% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/VerifiableTextField.kt index 6177b04be..838ecd806 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/VerifiableTextField.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.validation.views +package edu.stanford.spezi.core.design.validation.validation.views import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine +import edu.stanford.spezi.core.design.validation.validation.configuration.LocalValidationEngine enum class TextFieldType { TEXT, SECURE diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/compositionLocal/ProcessingDebounceDuration.kt similarity index 71% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/compositionLocal/ProcessingDebounceDuration.kt index 2c96fa2a8..86318136c 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/compositionLocal/ProcessingDebounceDuration.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.views.compositionLocal +package edu.stanford.spezi.core.design.validation.views.compositionLocal import androidx.compose.runtime.compositionLocalOf import kotlin.time.Duration.Companion.milliseconds diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/layout/DescriptionGridRow.kt similarity index 96% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/layout/DescriptionGridRow.kt index 230ba0101..11689d46a 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/layout/DescriptionGridRow.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.views.layout +package edu.stanford.spezi.core.design.validation.views.layout import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/OperationState.kt similarity index 50% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/OperationState.kt index e252a1615..3b2380d32 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/OperationState.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.views.model +package edu.stanford.spezi.core.design.validation.views.model interface OperationState { val representation: ViewState diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/ViewState.kt similarity index 90% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/ViewState.kt index a73b14109..a32b361b9 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/ViewState.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.views.model +package edu.stanford.spezi.core.design.validation.views.model import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/OperationStateAlert.kt similarity index 73% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/OperationStateAlert.kt index cb2eb4abb..07b6086e0 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/OperationStateAlert.kt @@ -1,10 +1,10 @@ -package edu.stanford.spezi.core.design.views.views.viewModifier.viewState +package edu.stanford.spezi.core.design.validation.views.viewModifier.viewState import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import edu.stanford.spezi.core.design.views.views.model.OperationState +import edu.stanford.spezi.core.design.validation.views.model.OperationState @Composable fun OperationStateAlert( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateAlert.kt similarity index 76% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateAlert.kt index 43455d706..df5891385 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateAlert.kt @@ -1,11 +1,12 @@ -package edu.stanford.spezi.core.design.views.views.viewModifier.viewState +package edu.stanford.spezi.core.design.validation.views.viewModifier.viewState import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.validation.views.model.ViewState @Composable fun ViewStateAlert(state: MutableState) { @@ -26,7 +27,7 @@ fun ViewStateAlert(state: MutableState) { state.value = ViewState.Idle } ) { - Text("Okay") + Text(StringResource("OK").text()) } } ) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateMapper.kt similarity index 60% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateMapper.kt index 1ca174b1d..bff40fbad 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateMapper.kt @@ -1,10 +1,10 @@ -package edu.stanford.spezi.core.design.views.views.viewModifier.viewState +package edu.stanford.spezi.core.design.validation.views.viewModifier.viewState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import edu.stanford.spezi.core.design.views.views.model.OperationState -import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.core.design.validation.views.model.OperationState +import edu.stanford.spezi.core.design.validation.views.model.ViewState @Composable fun MapOperationStateToViewState(state: State, viewState: MutableState) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/ProcessingOverlay.kt similarity index 60% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/ProcessingOverlay.kt index b6cf3357b..b78c7dddf 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/ProcessingOverlay.kt @@ -1,11 +1,15 @@ -package edu.stanford.spezi.core.design.views.views.views.button +package edu.stanford.spezi.core.design.validation.views.views.button +import androidx.compose.animation.core.animate import androidx.compose.foundation.layout.Box import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.core.design.validation.views.model.ViewState @Composable fun ProcessingOverlay( @@ -26,8 +30,15 @@ fun ProcessingOverlay( processingContent: @Composable () -> Unit = { CircularProgressIndicator() }, content: @Composable () -> Unit, ) { + val alpha = remember { mutableFloatStateOf(0f) } + LaunchedEffect(isProcessing) { + val newValue = if (isProcessing) 0f else 1f + animate(1f - newValue, newValue) { value, _ -> + alpha.floatValue = value + } + } Box { - Box(Modifier.alpha(if (isProcessing) 0f else 1f)) { + Box(Modifier.alpha(alpha.floatValue)) { content() } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/SuspendButton.kt similarity index 76% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/SuspendButton.kt index 7efbf01a3..2239a953c 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/SuspendButton.kt @@ -1,14 +1,15 @@ -package edu.stanford.spezi.core.design.views.views.views.button +package edu.stanford.spezi.core.design.validation.views.views.button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import edu.stanford.spezi.core.design.component.Button -import edu.stanford.spezi.core.design.views.views.compositionLocal.LocalProcessingDebounceDuration -import edu.stanford.spezi.core.design.views.views.model.ViewState -import kotlinx.coroutines.cancel +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.validation.views.compositionLocal.LocalProcessingDebounceDuration +import edu.stanford.spezi.core.design.validation.views.model.ViewState import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -19,7 +20,18 @@ private enum class SuspendButtonState { @Composable fun SuspendButton( - state: MutableState, + title: StringResource, + state: MutableState = remember { mutableStateOf(ViewState.Idle) }, + action: suspend () -> Unit, +) { + SuspendButton(state, action) { + Text(title.text()) + } +} + +@Composable +fun SuspendButton( + state: MutableState = remember { mutableStateOf(ViewState.Idle) }, action: suspend () -> Unit, label: @Composable () -> Unit, ) { @@ -43,16 +55,13 @@ fun SuspendButton( } } - // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? state.value = ViewState.Processing - coroutineScope.launch { runCatching { action() debounceJob.cancel() if (state.value != ViewState.Idle) { - // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? state.value = ViewState.Idle } }.onFailure { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/text/Markdown.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/text/Markdown.kt new file mode 100644 index 000000000..0e865340d --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/text/Markdown.kt @@ -0,0 +1,82 @@ +package edu.stanford.spezi.core.design.validation.views.views.text + +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent +import edu.stanford.spezi.core.design.component.markdown.MarkdownElement +import edu.stanford.spezi.core.design.component.markdown.MarkdownParser +import edu.stanford.spezi.core.design.validation.views.model.ViewState +import java.nio.charset.StandardCharsets + +@Composable +fun MarkdownBytes( + bytes: ByteArray, + state: MutableState = remember { mutableStateOf(ViewState.Idle) }, +) { + MarkdownBytes( + bytes = { bytes }, + state = state, + ) +} + +@Composable +fun MarkdownBytes( + bytes: suspend () -> ByteArray, + state: MutableState = remember { mutableStateOf(ViewState.Idle) }, +) { + MarkdownString( + string = { bytes().toString(StandardCharsets.UTF_8) }, + state = state, + ) +} + +@Composable +fun MarkdownString( + string: String, + state: MutableState = remember { mutableStateOf(ViewState.Idle) }, +) { + MarkdownString( + string = { string }, + state = state, + ) +} + +@Composable +fun MarkdownString( + string: suspend () -> String, + state: MutableState = remember { mutableStateOf(ViewState.Idle) }, +) { + Markdown( + build = { MarkdownParser().parse(string()) }, + state = state, + ) +} + +@Composable +fun Markdown( + build: suspend () -> List, + state: MutableState = remember { mutableStateOf(ViewState.Idle) }, +) { + var markdownContent by remember { mutableStateOf?>(null) } + + @Suppress("detekt:TooGenericExceptionCaught") + LaunchedEffect(Unit) { + state.value = ViewState.Processing + try { + markdownContent = build() + state.value = ViewState.Idle + } catch (throwable: Throwable) { + state.value = ViewState.Error(throwable) + } + } + + markdownContent?.let { + MarkdownComponent(it) + } ?: CircularProgressIndicator() +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt deleted file mode 100644 index f02fdc40a..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt +++ /dev/null @@ -1,6 +0,0 @@ -package edu.stanford.spezi.core.design.views.validation.configuration - -import androidx.compose.runtime.compositionLocalOf -import edu.stanford.spezi.core.design.views.validation.ValidationEngine - -val LocalValidationEngine = compositionLocalOf { null } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt deleted file mode 100644 index 30b193192..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt +++ /dev/null @@ -1,9 +0,0 @@ -package edu.stanford.spezi.core.design.views.validation.configuration - -import androidx.compose.runtime.compositionLocalOf -import edu.stanford.spezi.core.design.views.validation.ValidationEngine -import edu.stanford.spezi.core.design.views.validation.ValidationEngineConfiguration - -val LocalValidationEngineConfiguration = compositionLocalOf { - ValidationEngineConfiguration.noneOf(ValidationEngine.ConfigurationOption::class.java) -} diff --git a/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt b/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt index fd7c4203d..b2c8200ae 100644 --- a/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt +++ b/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt @@ -1,9 +1,9 @@ package edu.stanford.spezi.core.design import com.google.common.truth.Truth.assertThat -import edu.stanford.spezi.core.design.views.validation.ValidationEngineImpl -import edu.stanford.spezi.core.design.views.validation.ValidationRule -import edu.stanford.spezi.core.design.views.validation.nonEmpty +import edu.stanford.spezi.core.design.validation.validation.ValidationEngineImpl +import edu.stanford.spezi.core.design.validation.validation.ValidationRule +import edu.stanford.spezi.core.design.validation.validation.nonEmpty import org.junit.Test class SpeziValidationTest { diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt index 7db3a40cc..d13d0e6ae 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.test.platform.app.InstrumentationRegistry import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents import edu.stanford.spezi.core.testing.assertImageIdentifier import edu.stanford.spezi.core.testing.onNodeWithIdentifier import edu.stanford.spezi.modules.contact.ContactComposableTestIdentifier From 306b46bf6c47b6ce16805bf1808a123cf75f7c25 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 13:46:54 -0800 Subject: [PATCH 08/51] Rename test --- ...ionTest.kt => FocusValidationRulesTest.kt} | 4 +- .../composables/DefaultValidationRules.kt | 38 ------------------- 2 files changed, 2 insertions(+), 40 deletions(-) rename core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/{ValidationTest.kt => FocusValidationRulesTest.kt} (93%) delete mode 100644 core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/DefaultValidationRules.kt diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/ValidationTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/FocusValidationRulesTest.kt similarity index 93% rename from core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/ValidationTest.kt rename to core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/FocusValidationRulesTest.kt index 340a5d733..3466f8c26 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/ValidationTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/FocusValidationRulesTest.kt @@ -7,7 +7,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test -class ValidationTest { +class FocusValidationRulesTest { @get:Rule val composeTestRule = createComposeRule() @@ -20,7 +20,7 @@ class ValidationTest { } @Test - fun testValidationWithFocus() { + fun testFocusValidationRules() { focusValidationRules { assertHasEngines(true) assertInputValid(false) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/DefaultValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/DefaultValidationRules.kt deleted file mode 100644 index 55b0bb61c..000000000 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/DefaultValidationRules.kt +++ /dev/null @@ -1,38 +0,0 @@ -package edu.stanford.spezi.core.design.validation.composables - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.validation.Validate -import edu.stanford.spezi.core.design.validation.validation.ValidationRule -import edu.stanford.spezi.core.design.validation.validation.asciiLettersOnly -import edu.stanford.spezi.core.design.validation.validation.mediumPassword -import edu.stanford.spezi.core.design.validation.validation.minimalEmail -import edu.stanford.spezi.core.design.validation.validation.minimalPassword -import edu.stanford.spezi.core.design.validation.validation.nonEmpty -import edu.stanford.spezi.core.design.validation.validation.strongPassword -import edu.stanford.spezi.core.design.validation.validation.unicodeLettersOnly -import edu.stanford.spezi.core.design.validation.validation.views.VerifiableTextField - -@Composable -fun DefaultValidationRules() { - val input = remember { mutableStateOf("") } - val rules = remember { - listOf( - ValidationRule.nonEmpty, - ValidationRule.unicodeLettersOnly, - ValidationRule.asciiLettersOnly, - ValidationRule.minimalEmail, - ValidationRule.minimalPassword, - ValidationRule.mediumPassword, - ValidationRule.strongPassword - ) - } - Validate(input.value, rules) { - VerifiableTextField( - StringResource("Field"), - text = input - ) - } -} From dc8133b43c2895069c61f076ff718f7fa13664f2 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 13:59:45 -0800 Subject: [PATCH 09/51] Fix import issues --- .../edu/stanford/spezi/modules/contact/ContactComposable.kt | 2 +- .../kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt index 5da9e7e63..59d36d12a 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt @@ -32,12 +32,12 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents import edu.stanford.spezi.core.utils.extensions.testIdentifier import edu.stanford.spezi.modules.contact.component.AddressCard import edu.stanford.spezi.modules.contact.component.ContactOptionCard import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.call import edu.stanford.spezi.modules.contact.model.email import edu.stanford.spezi.modules.contact.model.text diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt index 6d0ffefec..68c62e381 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt @@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents import java.util.UUID /** From 4258420c7f2dc29067b7a6ea4525c2c46137dba5 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 14:05:16 -0800 Subject: [PATCH 10/51] Remove import issues --- .../bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt | 2 +- .../edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt | 2 +- .../engagehf/contact/data/ContactDocumentToContactMapperTest.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt index a5bc024ca..cd07937fb 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt @@ -2,9 +2,9 @@ package edu.stanford.bdh.engagehf.contact.data import com.google.firebase.firestore.DocumentSnapshot import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.call import edu.stanford.spezi.modules.contact.model.email import javax.inject.Inject diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt index 43233085c..66966444d 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt @@ -29,11 +29,11 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents import edu.stanford.spezi.core.notification.R import edu.stanford.spezi.modules.contact.ContactComposable import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.call import edu.stanford.spezi.modules.contact.model.email import edu.stanford.spezi.modules.contact.model.website diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt index d60bc828f..b3634a52f 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt @@ -3,7 +3,7 @@ package edu.stanford.bdh.engagehf.contact.data import com.google.common.truth.Truth.assertThat import com.google.firebase.firestore.DocumentSnapshot import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.modules.contact.model.PersonNameComponents +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents import io.mockk.every import io.mockk.mockk import org.junit.Test From e3baa1b0267f9328157cff46640d09dcbbd3e1ef Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 14:11:58 -0800 Subject: [PATCH 11/51] Add import --- .../kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt index 1a32dc9a1..2038e79dd 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt @@ -5,9 +5,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.call import edu.stanford.spezi.modules.contact.model.email import edu.stanford.spezi.modules.contact.model.text From 0a94bc5ffc42574232b300d25db6294b69b3a415 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 14:17:40 -0800 Subject: [PATCH 12/51] Move files into the correct repositories --- .../data/ContactDocumentToContactMapper.kt | 13 +++++++------ .../bdh/engagehf/contact/ui/ContactScreen.kt | 7 +++++-- .../data/ContactDocumentToContactMapperTest.kt | 4 ++-- .../composables/NameFieldsTestComposable.kt | 8 ++++---- .../composables/UserProfileTestComposable.kt | 12 +++++++++--- .../personalInfo/PersonNameComponents.kt | 12 ++++++------ .../personalInfo/UserProfileComposable.kt | 6 +++--- .../personalInfo/fields/NameFieldRow.kt | 18 +++++++++--------- .../personalInfo/fields/NameTextField.kt | 16 ++++++++-------- .../validation/CascadingValidationEffect.kt | 2 +- .../validation/ValidationEngine.kt | 2 +- .../validation/ValidationModifier.kt | 2 +- .../validation/ValidationRule.kt | 2 +- .../validation/ValidationRuleDefaults.kt | 2 +- .../ValidationDebounceDuration.kt | 2 +- .../configuration/ValidationEngine.kt | 2 +- .../ValidationEngineConfiguration.kt | 2 +- .../state/CapturedValidationState.kt | 2 +- .../state/CapturedValidationStateEntries.kt | 2 +- .../validation/state/FailedValidationResult.kt | 2 +- .../validation/state/ReceiveValidation.kt | 2 +- .../validation/state/ValidationContext.kt | 2 +- .../views/ValidationResultsComposable.kt | 2 +- .../validation/views/VerifiableTextField.kt | 2 +- .../ProcessingDebounceDuration.kt | 2 +- .../views/layout/DescriptionGridRow.kt | 2 +- .../views/model/OperationState.kt | 2 +- .../views/model/ViewState.kt | 2 +- .../viewState/OperationStateAlert.kt | 2 +- .../viewModifier/viewState/ViewStateAlert.kt | 2 +- .../viewModifier/viewState/ViewStateMapper.kt | 2 +- .../views/views/button/ProcessingOverlay.kt | 2 +- .../views/views/button/SuspendButton.kt | 2 +- .../views/views/text/Markdown.kt | 2 +- .../spezi/modules/contact/ContactFactory.kt | 12 +++++++++--- .../simulator/ContactComposableSimulator.kt | 4 ++-- .../spezi/modules/contact/ContactComposable.kt | 7 +++++-- .../spezi/modules/contact/model/Contact.kt | 4 ++-- 38 files changed, 96 insertions(+), 77 deletions(-) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/personalInfo/PersonNameComponents.kt (53%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/personalInfo/UserProfileComposable.kt (91%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/personalInfo/fields/NameFieldRow.kt (65%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/personalInfo/fields/NameTextField.kt (64%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/CascadingValidationEffect.kt (51%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/ValidationEngine.kt (98%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/ValidationModifier.kt (97%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/ValidationRule.kt (95%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/ValidationRuleDefaults.kt (96%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/configuration/ValidationDebounceDuration.kt (77%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/configuration/ValidationEngine.kt (73%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/configuration/ValidationEngineConfiguration.kt (83%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/state/CapturedValidationState.kt (92%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/state/CapturedValidationStateEntries.kt (87%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/state/FailedValidationResult.kt (88%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/state/ReceiveValidation.kt (91%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/state/ValidationContext.kt (95%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/views/ValidationResultsComposable.kt (91%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/validation/views/VerifiableTextField.kt (97%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/compositionLocal/ProcessingDebounceDuration.kt (71%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/layout/DescriptionGridRow.kt (96%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/model/OperationState.kt (50%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/model/ViewState.kt (90%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/viewModifier/viewState/OperationStateAlert.kt (86%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/viewModifier/viewState/ViewStateAlert.kt (92%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/viewModifier/viewState/ViewStateMapper.kt (86%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/views/button/ProcessingOverlay.kt (95%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/views/button/SuspendButton.kt (97%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/{validation => views}/views/views/text/Markdown.kt (97%) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt index cd07937fb..b87f3394d 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt @@ -2,7 +2,7 @@ package edu.stanford.bdh.engagehf.contact.data import com.google.firebase.firestore.DocumentSnapshot import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption import edu.stanford.spezi.modules.contact.model.call @@ -19,11 +19,12 @@ class ContactDocumentToContactMapper @Inject constructor() { } val components = contactName.split(", ") val nameComponents = components.firstOrNull()?.split(" ") - val personNameComponents = PersonNameComponents( - givenName = nameComponents?.getOrNull(0), - familyName = nameComponents?.drop(1) - ?.joinToString(" ") // assigning everything besides given name here - ) + val personNameComponents = + edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + givenName = nameComponents?.getOrNull(0), + familyName = nameComponents?.drop(1) + ?.joinToString(" ") // assigning everything besides given name here + ) val title = components.lastOrNull() val contactEmail = document.getString(CONTACT_EMAIL_FIELD) val phone = document.getString(CONTACT_PHONE_FIELD) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt index 66966444d..bce489e51 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt @@ -29,7 +29,7 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import edu.stanford.spezi.core.notification.R import edu.stanford.spezi.modules.contact.ContactComposable import edu.stanford.spezi.modules.contact.model.Contact @@ -109,7 +109,10 @@ private class ContactUiStateProvider : PreviewParameterProvider listOfNotNull( + edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.LONG -> listOfNotNull( namePrefix, givenName, nickname?.let { "\"$it\"" }, @@ -22,11 +22,11 @@ data class PersonNameComponents( familyName, nameSuffix ).joinToString(" ") - FormatStyle.MEDIUM -> + edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.MEDIUM -> TODO("Not yet implemented.") - FormatStyle.SHORT -> + edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.SHORT -> TODO("Not yet implemented.") - FormatStyle.ABBREVIATED -> listOfNotNull( + edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.ABBREVIATED -> listOfNotNull( givenName, middleName, familyName, diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt similarity index 91% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/UserProfileComposable.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt index 77e8db0d2..eb14be993 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.personalInfo +package edu.stanford.spezi.core.design.views.personalInfo import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -30,7 +30,7 @@ import kotlin.math.min @Composable fun UserProfileComposable( modifier: Modifier = Modifier, - name: PersonNameComponents, + name: edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents, imageLoader: suspend () -> ImageResource? = { null }, ) { var size by remember { mutableStateOf(IntSize.Zero) } @@ -43,7 +43,7 @@ fun UserProfileComposable( } val formattedName = remember(name) { - name.formatted(PersonNameComponents.FormatStyle.ABBREVIATED) + name.formatted(edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.ABBREVIATED) } Box(modifier.onSizeChanged { size = it }.aspectRatio(1f)) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt similarity index 65% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameFieldRow.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt index 3c4f5e046..2a0c8ebbc 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameFieldRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.personalInfo.fields +package edu.stanford.spezi.core.design.views.personalInfo.fields import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -12,15 +12,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import edu.stanford.spezi.core.design.validation.views.layout.DescriptionGridRow import kotlin.reflect.KMutableProperty1 @Composable fun NameFieldRow( description: StringResource, - name: MutableState, - component: KMutableProperty1, + name: MutableState, + component: KMutableProperty1, label: @Composable () -> Unit, ) { NameFieldRow( @@ -33,8 +33,8 @@ fun NameFieldRow( @Composable fun NameFieldRow( - name: MutableState, - component: KMutableProperty1, + name: MutableState, + component: KMutableProperty1, description: @Composable () -> Unit, label: @Composable () -> Unit, ) { @@ -51,12 +51,12 @@ fun NameFieldRow( @ThemePreviews @Composable private fun NameFieldRowPreview() { - val name = remember { mutableStateOf(PersonNameComponents()) } + val name = remember { mutableStateOf(edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents()) } Column { NameFieldRow( name, - PersonNameComponents::givenName, + edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents::givenName, description = { Text("First") } ) { Text("enter first name") @@ -67,7 +67,7 @@ private fun NameFieldRowPreview() { // Last Name Field NameFieldRow( name, - PersonNameComponents::familyName, + edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents::familyName, description = { Text("Last") } ) { Text("enter last name") diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt similarity index 64% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameTextField.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt index 2b0528625..6fc5a6304 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/personalInfo/fields/NameTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.personalInfo.fields +package edu.stanford.spezi.core.design.views.personalInfo.fields import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text @@ -9,14 +9,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import kotlin.reflect.KMutableProperty1 @Composable fun NameTextField( label: StringResource, - name: MutableState, - component: KMutableProperty1, + name: MutableState, + component: KMutableProperty1, prompt: StringResource? = null, ) { NameTextField(name, component, prompt) { @@ -26,8 +26,8 @@ fun NameTextField( @Composable fun NameTextField( - name: MutableState, - component: KMutableProperty1, + name: MutableState, + component: KMutableProperty1, prompt: StringResource? = null, label: @Composable () -> Unit, ) { @@ -53,9 +53,9 @@ fun NameTextField( @ThemePreviews @Composable private fun NameTextFieldPreview() { - val name = remember { mutableStateOf(PersonNameComponents()) } + val name = remember { mutableStateOf(edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents()) } - NameTextField(name, PersonNameComponents::givenName) { + NameTextField(name, edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents::givenName) { Text("Enter first name") } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/CascadingValidationEffect.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt similarity index 51% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/CascadingValidationEffect.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt index 75ba0a587..e1676ff23 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/CascadingValidationEffect.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/CascadingValidationEffect.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation +package edu.stanford.spezi.core.design.views.validation enum class CascadingValidationEffect { CONTINUE, INTERCEPT diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt similarity index 98% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationEngine.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt index 39f333125..cfadd374a 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationEngine.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation +package edu.stanford.spezi.core.design.views.validation import androidx.compose.runtime.mutableStateOf import edu.stanford.spezi.core.design.validation.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationModifier.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt similarity index 97% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationModifier.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt index 7c7019f60..3546772ae 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationModifier.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation +package edu.stanford.spezi.core.design.views.validation import android.annotation.SuppressLint import androidx.compose.runtime.Composable diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRule.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt similarity index 95% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRule.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt index e488f4da3..cadde2fcb 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRule.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation +package edu.stanford.spezi.core.design.views.validation import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.validation.validation.state.FailedValidationResult diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRuleDefaults.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt similarity index 96% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRuleDefaults.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt index 791d4c37f..a6aa86bc7 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/ValidationRuleDefaults.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation +package edu.stanford.spezi.core.design.views.validation import edu.stanford.spezi.core.design.component.StringResource import java.nio.charset.StandardCharsets diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationDebounceDuration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt similarity index 77% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationDebounceDuration.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt index 000da7721..15cef5d76 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationDebounceDuration.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.configuration +package edu.stanford.spezi.core.design.views.validation.configuration import androidx.compose.runtime.compositionLocalOf import kotlin.time.Duration.Companion.seconds diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt similarity index 73% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngine.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt index 4ba7077af..da8284914 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngine.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.configuration +package edu.stanford.spezi.core.design.views.validation.configuration import androidx.compose.runtime.compositionLocalOf import edu.stanford.spezi.core.design.validation.validation.ValidationEngine diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngineConfiguration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt similarity index 83% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngineConfiguration.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt index 117c756e5..dfd0a7cd2 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/configuration/ValidationEngineConfiguration.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.configuration +package edu.stanford.spezi.core.design.views.validation.configuration import androidx.compose.runtime.compositionLocalOf import edu.stanford.spezi.core.design.validation.validation.ValidationEngine diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt similarity index 92% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationState.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt index c04602159..a9aa8de9b 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.state +package edu.stanford.spezi.core.design.views.validation.state import androidx.compose.runtime.MutableState import edu.stanford.spezi.core.design.validation.validation.ValidationEngine diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationStateEntries.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt similarity index 87% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationStateEntries.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt index 949a771b3..50d5e1583 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/CapturedValidationStateEntries.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationStateEntries.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.state +package edu.stanford.spezi.core.design.views.validation.state import androidx.compose.runtime.compositionLocalOf diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/FailedValidationResult.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt similarity index 88% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/FailedValidationResult.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt index 35bc2b51c..14b4cf8a0 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/FailedValidationResult.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.state +package edu.stanford.spezi.core.design.views.validation.state import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.validation.validation.ValidationRule diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ReceiveValidation.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt similarity index 91% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ReceiveValidation.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt index bddafd645..94ce4e850 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ReceiveValidation.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ReceiveValidation.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.state +package edu.stanford.spezi.core.design.views.validation.state import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ValidationContext.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt similarity index 95% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ValidationContext.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt index 18873e6a8..a12a71d86 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/state/ValidationContext.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.state +package edu.stanford.spezi.core.design.views.validation.state data class ValidationContext internal constructor( private val entries: List = emptyList(), diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/ValidationResultsComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt similarity index 91% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/ValidationResultsComposable.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt index 29fb9bb0b..b374c4be3 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/ValidationResultsComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.views +package edu.stanford.spezi.core.design.views.validation.views import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt similarity index 97% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/VerifiableTextField.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index 838ecd806..53623862e 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.validation.views +package edu.stanford.spezi.core.design.views.validation.views import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/compositionLocal/ProcessingDebounceDuration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt similarity index 71% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/compositionLocal/ProcessingDebounceDuration.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt index 86318136c..2c96fa2a8 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/compositionLocal/ProcessingDebounceDuration.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.compositionLocal +package edu.stanford.spezi.core.design.views.views.compositionLocal import androidx.compose.runtime.compositionLocalOf import kotlin.time.Duration.Companion.milliseconds diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/layout/DescriptionGridRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt similarity index 96% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/layout/DescriptionGridRow.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt index 11689d46a..230ba0101 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/layout/DescriptionGridRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.layout +package edu.stanford.spezi.core.design.views.views.layout import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/OperationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt similarity index 50% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/OperationState.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt index 3b2380d32..e252a1615 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/OperationState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/OperationState.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.model +package edu.stanford.spezi.core.design.views.views.model interface OperationState { val representation: ViewState diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/ViewState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt similarity index 90% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/ViewState.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt index a32b361b9..a73b14109 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/model/ViewState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.model +package edu.stanford.spezi.core.design.views.views.model import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/OperationStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt similarity index 86% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/OperationStateAlert.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt index 07b6086e0..a5b5208a2 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/OperationStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.viewModifier.viewState +package edu.stanford.spezi.core.design.views.views.viewModifier.viewState import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt similarity index 92% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateAlert.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt index df5891385..f3c2b44a2 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.viewModifier.viewState +package edu.stanford.spezi.core.design.views.views.viewModifier.viewState import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateMapper.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt similarity index 86% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateMapper.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt index bff40fbad..6005db697 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/viewModifier/viewState/ViewStateMapper.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.viewModifier.viewState +package edu.stanford.spezi.core.design.views.views.viewModifier.viewState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/ProcessingOverlay.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt similarity index 95% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/ProcessingOverlay.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt index b78c7dddf..c84894061 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/ProcessingOverlay.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.views.button +package edu.stanford.spezi.core.design.views.views.views.button import androidx.compose.animation.core.animate import androidx.compose.foundation.layout.Box diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/SuspendButton.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt similarity index 97% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/SuspendButton.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt index 2239a953c..0e4311f61 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/button/SuspendButton.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.views.button +package edu.stanford.spezi.core.design.views.views.views.button import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/text/Markdown.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt similarity index 97% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/text/Markdown.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt index 0e865340d..9b2451242 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/validation/views/views/text/Markdown.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.validation.views.views.text +package edu.stanford.spezi.core.design.views.views.views.text import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt index 2038e79dd..1116c5f9e 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt @@ -5,7 +5,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption import edu.stanford.spezi.modules.contact.model.call @@ -16,7 +16,10 @@ import java.util.Locale object ContactFactory { val leland = Contact( - name = PersonNameComponents(givenName = "Leland", familyName = "Stanford"), + name = edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + givenName = "Leland", + familyName = "Stanford" + ), image = ImageResource.Vector(Icons.Default.AccountBox), title = StringResource("University Founder"), description = StringResource(""" @@ -42,7 +45,10 @@ He and his wife Jane were also the founders of Stanford University, which they n ) val mock = Contact( - name = PersonNameComponents(givenName = "Paul", familyName = "Schmiedmayer"), + name = edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + givenName = "Paul", + familyName = "Schmiedmayer" + ), image = ImageResource.Vector(Icons.Default.AccountBox), title = StringResource("A Title"), description = StringResource(""" diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt index d13d0e6ae..c137d742f 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.test.platform.app.InstrumentationRegistry import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import edu.stanford.spezi.core.testing.assertImageIdentifier import edu.stanford.spezi.core.testing.onNodeWithIdentifier import edu.stanford.spezi.modules.contact.ContactComposableTestIdentifier @@ -53,7 +53,7 @@ class ContactComposableSimulator( } } - fun assertHasName(text: PersonNameComponents?) { + fun assertHasName(text: edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents?) { text?.let { name.assertExists() .assertTextEquals(it.formatted()) diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt index 59d36d12a..d7a1f10f6 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt @@ -32,7 +32,7 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import edu.stanford.spezi.core.utils.extensions.testIdentifier import edu.stanford.spezi.modules.contact.component.AddressCard import edu.stanford.spezi.modules.contact.component.ContactOptionCard @@ -197,7 +197,10 @@ private object ContactComposableFactory { ), ): Contact { return Contact( - name = PersonNameComponents(givenName = "Leland", familyName = "Stanford"), + name = edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + givenName = "Leland", + familyName = "Stanford" + ), image = ImageResource.Vector(Icons.Default.AccountBox), title = title, description = description, diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt index 68c62e381..b78ce225d 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt @@ -5,7 +5,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import java.util.UUID /** @@ -24,7 +24,7 @@ import java.util.UUID */ data class Contact( val id: UUID = UUID.randomUUID(), - val name: PersonNameComponents, + val name: edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents, val image: ImageResource = ImageResource.Vector(Icons.Default.AccountBox), val title: StringResource? = null, val description: StringResource? = null, From 40601b49b7f1ad4e51f3393ff2eebf0df6b505e2 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 14:28:18 -0800 Subject: [PATCH 13/51] Rename package --- .../contact/data/ContactDocumentToContactMapper.kt | 2 +- .../bdh/engagehf/contact/ui/ContactScreen.kt | 2 +- .../composables/NameFieldsTestComposable.kt | 8 ++++---- .../composables/UserProfileTestComposable.kt | 6 +++--- .../views/composables/MarkdownTestComposable.kt | 4 ++-- .../views/composables/SuspendButtonTestComposable.kt | 6 +++--- .../views/personalInfo/PersonNameComponents.kt | 10 +++++----- .../views/personalInfo/UserProfileComposable.kt | 4 ++-- .../design/views/personalInfo/fields/NameFieldRow.kt | 12 ++++++------ .../views/personalInfo/fields/NameTextField.kt | 12 ++++++------ .../core/design/views/validation/ValidationEngine.kt | 4 ++-- .../design/views/validation/ValidationModifier.kt | 10 +++++----- .../core/design/views/validation/ValidationRule.kt | 2 +- .../validation/configuration/ValidationEngine.kt | 2 +- .../configuration/ValidationEngineConfiguration.kt | 4 ++-- .../validation/state/CapturedValidationState.kt | 2 +- .../views/validation/state/FailedValidationResult.kt | 2 +- .../validation/views/ValidationResultsComposable.kt | 2 +- .../views/validation/views/VerifiableTextField.kt | 2 +- .../viewModifier/viewState/OperationStateAlert.kt | 2 +- .../views/viewModifier/viewState/ViewStateAlert.kt | 2 +- .../views/viewModifier/viewState/ViewStateMapper.kt | 4 ++-- .../views/views/views/button/ProcessingOverlay.kt | 2 +- .../design/views/views/views/button/SuspendButton.kt | 4 ++-- .../core/design/views/views/views/text/Markdown.kt | 2 +- .../contact/simulator/ContactComposableSimulator.kt | 2 +- .../spezi/modules/contact/ContactComposable.kt | 2 +- .../stanford/spezi/modules/contact/model/Contact.kt | 2 +- 28 files changed, 59 insertions(+), 59 deletions(-) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt index b87f3394d..86489e526 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt @@ -20,7 +20,7 @@ class ContactDocumentToContactMapper @Inject constructor() { val components = contactName.split(", ") val nameComponents = components.firstOrNull()?.split(" ") val personNameComponents = - edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + PersonNameComponents( givenName = nameComponents?.getOrNull(0), familyName = nameComponents?.drop(1) ?.joinToString(" ") // assigning everything besides given name here diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt index bce489e51..a1d43bb36 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt @@ -109,7 +109,7 @@ private class ContactUiStateProvider : PreviewParameterProvider listOfNotNull( + FormatStyle.LONG -> listOfNotNull( namePrefix, givenName, nickname?.let { "\"$it\"" }, @@ -22,11 +22,11 @@ data class PersonNameComponents( familyName, nameSuffix ).joinToString(" ") - edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.MEDIUM -> + FormatStyle.MEDIUM -> TODO("Not yet implemented.") - edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.SHORT -> + FormatStyle.SHORT -> TODO("Not yet implemented.") - edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.ABBREVIATED -> listOfNotNull( + FormatStyle.ABBREVIATED -> listOfNotNull( givenName, middleName, familyName, diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt index eb14be993..7e539998e 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt @@ -30,7 +30,7 @@ import kotlin.math.min @Composable fun UserProfileComposable( modifier: Modifier = Modifier, - name: edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents, + name: PersonNameComponents, imageLoader: suspend () -> ImageResource? = { null }, ) { var size by remember { mutableStateOf(IntSize.Zero) } @@ -43,7 +43,7 @@ fun UserProfileComposable( } val formattedName = remember(name) { - name.formatted(edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents.FormatStyle.ABBREVIATED) + name.formatted(PersonNameComponents.FormatStyle.ABBREVIATED) } Box(modifier.onSizeChanged { size = it }.aspectRatio(1f)) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt index 2a0c8ebbc..0599319ca 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt @@ -13,14 +13,14 @@ import androidx.compose.ui.unit.dp import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents -import edu.stanford.spezi.core.design.validation.views.layout.DescriptionGridRow +import edu.stanford.spezi.core.design.views.views.layout.DescriptionGridRow import kotlin.reflect.KMutableProperty1 @Composable fun NameFieldRow( description: StringResource, - name: MutableState, - component: KMutableProperty1, + name: MutableState, + component: KMutableProperty1, label: @Composable () -> Unit, ) { NameFieldRow( @@ -33,8 +33,8 @@ fun NameFieldRow( @Composable fun NameFieldRow( - name: MutableState, - component: KMutableProperty1, + name: MutableState, + component: KMutableProperty1, description: @Composable () -> Unit, label: @Composable () -> Unit, ) { @@ -51,7 +51,7 @@ fun NameFieldRow( @ThemePreviews @Composable private fun NameFieldRowPreview() { - val name = remember { mutableStateOf(edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents()) } + val name = remember { mutableStateOf(PersonNameComponents()) } Column { NameFieldRow( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt index 6fc5a6304..5ffacc1cc 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt @@ -15,8 +15,8 @@ import kotlin.reflect.KMutableProperty1 @Composable fun NameTextField( label: StringResource, - name: MutableState, - component: KMutableProperty1, + name: MutableState, + component: KMutableProperty1, prompt: StringResource? = null, ) { NameTextField(name, component, prompt) { @@ -26,8 +26,8 @@ fun NameTextField( @Composable fun NameTextField( - name: MutableState, - component: KMutableProperty1, + name: MutableState, + component: KMutableProperty1, prompt: StringResource? = null, label: @Composable () -> Unit, ) { @@ -53,9 +53,9 @@ fun NameTextField( @ThemePreviews @Composable private fun NameTextFieldPreview() { - val name = remember { mutableStateOf(edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents()) } + val name = remember { mutableStateOf(PersonNameComponents()) } - NameTextField(name, edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents::givenName) { + NameTextField(name, PersonNameComponents::givenName) { Text("Enter first name") } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt index cfadd374a..42f50e87d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt @@ -1,8 +1,8 @@ package edu.stanford.spezi.core.design.views.validation import androidx.compose.runtime.mutableStateOf -import edu.stanford.spezi.core.design.validation.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION -import edu.stanford.spezi.core.design.validation.validation.state.FailedValidationResult +import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION +import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt index 3546772ae..5dec946f8 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt @@ -7,11 +7,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.validation.configuration.LocalValidationDebounce -import edu.stanford.spezi.core.design.validation.validation.configuration.LocalValidationEngine -import edu.stanford.spezi.core.design.validation.validation.configuration.LocalValidationEngineConfiguration -import edu.stanford.spezi.core.design.validation.validation.state.CapturedValidationState -import edu.stanford.spezi.core.design.validation.validation.state.LocalCapturedValidationStateEntries +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationDebounce +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngineConfiguration +import edu.stanford.spezi.core.design.views.validation.state.CapturedValidationState +import edu.stanford.spezi.core.design.views.validation.state.LocalCapturedValidationStateEntries @Composable fun Validate( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt index cadde2fcb..a4f61f9f1 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt @@ -1,7 +1,7 @@ package edu.stanford.spezi.core.design.views.validation import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.validation.state.FailedValidationResult +import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult import edu.stanford.spezi.core.utils.UUID import java.util.UUID diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt index da8284914..f02fdc40a 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt @@ -1,6 +1,6 @@ package edu.stanford.spezi.core.design.views.validation.configuration import androidx.compose.runtime.compositionLocalOf -import edu.stanford.spezi.core.design.validation.validation.ValidationEngine +import edu.stanford.spezi.core.design.views.validation.ValidationEngine val LocalValidationEngine = compositionLocalOf { null } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt index dfd0a7cd2..30b193192 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt @@ -1,8 +1,8 @@ package edu.stanford.spezi.core.design.views.validation.configuration import androidx.compose.runtime.compositionLocalOf -import edu.stanford.spezi.core.design.validation.validation.ValidationEngine -import edu.stanford.spezi.core.design.validation.validation.ValidationEngineConfiguration +import edu.stanford.spezi.core.design.views.validation.ValidationEngine +import edu.stanford.spezi.core.design.views.validation.ValidationEngineConfiguration val LocalValidationEngineConfiguration = compositionLocalOf { ValidationEngineConfiguration.noneOf(ValidationEngine.ConfigurationOption::class.java) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt index a9aa8de9b..5288bb5f7 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt @@ -1,7 +1,7 @@ package edu.stanford.spezi.core.design.views.validation.state import androidx.compose.runtime.MutableState -import edu.stanford.spezi.core.design.validation.validation.ValidationEngine +import edu.stanford.spezi.core.design.views.validation.ValidationEngine data class CapturedValidationState internal constructor( private val engine: ValidationEngine, diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt index 14b4cf8a0..f46992f52 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt @@ -1,7 +1,7 @@ package edu.stanford.spezi.core.design.views.validation.state import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.validation.ValidationRule +import edu.stanford.spezi.core.design.views.validation.ValidationRule import java.util.UUID data class FailedValidationResult( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt index b374c4be3..b07aabf20 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import edu.stanford.spezi.core.design.theme.TextStyles -import edu.stanford.spezi.core.design.validation.validation.state.FailedValidationResult +import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult @Composable fun ValidationResultsComposable( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index 53623862e..6177b04be 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.validation.configuration.LocalValidationEngine +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine enum class TextFieldType { TEXT, SECURE diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt index a5b5208a2..cb2eb4abb 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import edu.stanford.spezi.core.design.validation.views.model.OperationState +import edu.stanford.spezi.core.design.views.views.model.OperationState @Composable fun OperationStateAlert( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt index f3c2b44a2..8b4cf0b4d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.views.model.ViewState +import edu.stanford.spezi.core.design.views.views.model.ViewState @Composable fun ViewStateAlert(state: MutableState) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt index 6005db697..1ca174b1d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt @@ -3,8 +3,8 @@ package edu.stanford.spezi.core.design.views.views.viewModifier.viewState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import edu.stanford.spezi.core.design.validation.views.model.OperationState -import edu.stanford.spezi.core.design.validation.views.model.ViewState +import edu.stanford.spezi.core.design.views.views.model.OperationState +import edu.stanford.spezi.core.design.views.views.model.ViewState @Composable fun MapOperationStateToViewState(state: State, viewState: MutableState) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt index c84894061..85c0f4039 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import edu.stanford.spezi.core.design.validation.views.model.ViewState +import edu.stanford.spezi.core.design.views.views.model.ViewState @Composable fun ProcessingOverlay( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt index 0e4311f61..3383cafd6 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt @@ -8,8 +8,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import edu.stanford.spezi.core.design.component.Button import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.views.compositionLocal.LocalProcessingDebounceDuration -import edu.stanford.spezi.core.design.validation.views.model.ViewState +import edu.stanford.spezi.core.design.views.views.compositionLocal.LocalProcessingDebounceDuration +import edu.stanford.spezi.core.design.views.views.model.ViewState import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt index 9b2451242..85df3c7c8 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.setValue import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.component.markdown.MarkdownParser -import edu.stanford.spezi.core.design.validation.views.model.ViewState +import edu.stanford.spezi.core.design.views.views.model.ViewState import java.nio.charset.StandardCharsets @Composable diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt index c137d742f..7db3a40cc 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt @@ -53,7 +53,7 @@ class ContactComposableSimulator( } } - fun assertHasName(text: edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents?) { + fun assertHasName(text: PersonNameComponents?) { text?.let { name.assertExists() .assertTextEquals(it.formatted()) diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt index d7a1f10f6..9bede632d 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt @@ -197,7 +197,7 @@ private object ContactComposableFactory { ), ): Contact { return Contact( - name = edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + name = PersonNameComponents( givenName = "Leland", familyName = "Stanford" ), diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt index b78ce225d..c151b35b5 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt @@ -24,7 +24,7 @@ import java.util.UUID */ data class Contact( val id: UUID = UUID.randomUUID(), - val name: edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents, + val name: PersonNameComponents, val image: ImageResource = ImageResource.Vector(Icons.Default.AccountBox), val title: StringResource? = null, val description: StringResource? = null, From 728e5348a28239c71343739fc0fd42166406f4ad Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 14:30:05 -0800 Subject: [PATCH 14/51] Remove nonimported usages of PersonNameComponents --- .../contact/data/ContactDocumentToContactMapperTest.kt | 2 +- .../core/design/views/personalInfo/fields/NameFieldRow.kt | 4 ++-- .../edu/stanford/spezi/modules/contact/ContactFactory.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt index 7db9e3eb3..46737ea6c 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt @@ -60,7 +60,7 @@ class ContactDocumentToContactMapperTest { // then with(result) { assertThat(name).isEqualTo( - edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + PersonNameComponents( givenName = givenGivenName, familyName = givenFamilyName, ) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt index 0599319ca..48102fe1f 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt @@ -56,7 +56,7 @@ private fun NameFieldRowPreview() { Column { NameFieldRow( name, - edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents::givenName, + PersonNameComponents::givenName, description = { Text("First") } ) { Text("enter first name") @@ -67,7 +67,7 @@ private fun NameFieldRowPreview() { // Last Name Field NameFieldRow( name, - edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents::familyName, + PersonNameComponents::familyName, description = { Text("Last") } ) { Text("enter last name") diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt index 1116c5f9e..89630f527 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt @@ -16,7 +16,7 @@ import java.util.Locale object ContactFactory { val leland = Contact( - name = edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + name = PersonNameComponents( givenName = "Leland", familyName = "Stanford" ), @@ -45,7 +45,7 @@ He and his wife Jane were also the founders of Stanford University, which they n ) val mock = Contact( - name = edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents( + name = PersonNameComponents( givenName = "Paul", familyName = "Schmiedmayer" ), From 7928eb2a5997fc2421d99e48ecf23207b768f24f Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 15:23:17 -0800 Subject: [PATCH 15/51] Update previews --- .../personalInfo/PersonNameComponents.kt | 1 - .../personalInfo/UserProfileComposable.kt | 60 ++++++++++++++++++- .../views/personalInfo/fields/NameFieldRow.kt | 37 +++++++----- .../personalInfo/fields/NameTextField.kt | 16 +++-- .../views/validation/ValidationModifier.kt | 9 ++- .../validation/ValidationRuleDefaults.kt | 4 +- .../views/ValidationResultsComposable.kt | 18 ++++++ .../validation/views/VerifiableTextField.kt | 41 ++++++++++--- .../views/views/layout/DescriptionGridRow.kt | 3 +- .../viewModifier/viewState/ViewStateAlert.kt | 20 ++++++- .../views/views/button/ProcessingOverlay.kt | 22 ++++++- .../views/views/views/button/SuspendButton.kt | 13 ++++ .../design/views/views/views/text/Markdown.kt | 10 ++++ 13 files changed, 215 insertions(+), 39 deletions(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt index 471037d08..c87fe5045 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt @@ -28,7 +28,6 @@ data class PersonNameComponents( TODO("Not yet implemented.") FormatStyle.ABBREVIATED -> listOfNotNull( givenName, - middleName, familyName, ).joinToString("") .filter { it.isUpperCase() } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt index 7e539998e..acdea2f1d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -17,20 +19,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.ImageResourceComposable import edu.stanford.spezi.core.design.theme.Colors +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.theme.lighten import edu.stanford.spezi.core.logging.SpeziLogger import kotlin.math.min @Composable fun UserProfileComposable( - modifier: Modifier = Modifier, name: PersonNameComponents, + modifier: Modifier = Modifier, imageLoader: suspend () -> ImageResource? = { null }, ) { var size by remember { mutableStateOf(IntSize.Zero) } @@ -46,7 +52,10 @@ fun UserProfileComposable( name.formatted(PersonNameComponents.FormatStyle.ABBREVIATED) } - Box(modifier.onSizeChanged { size = it }.aspectRatio(1f)) { + Box( + modifier + .onSizeChanged { size = it } + .aspectRatio(1f)) { val sideLength = min(size.height, size.width).dp Box(modifier.size(sideLength, sideLength), contentAlignment = Alignment.Center) { loadedImage?.let { @@ -54,11 +63,15 @@ fun UserProfileComposable( it, "", // TODO: Add contentDescription to ImageResource directly? Modifier + .fillMaxSize() .clip(CircleShape) .background(Colors.background, CircleShape) ) } ?: run { - Box(Modifier.background(Colors.secondary, CircleShape).fillMaxSize(), contentAlignment = Alignment.Center) { + Box( + Modifier + .background(Colors.secondary, CircleShape) + .fillMaxSize(), contentAlignment = Alignment.Center) { Text( formattedName, fontSize = (sideLength.value * 0.2).sp, @@ -69,3 +82,44 @@ fun UserProfileComposable( } } } + +private typealias UserProfilePreviewData = Pair ImageResource?> +private class UserProfileProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + Pair( + PersonNameComponents( + givenName = "Paul", + familyName = "Schmiedmayer", + ) + ) { null }, + Pair( + PersonNameComponents( + namePrefix = "Prof.", + givenName = "Oliver", + middleName = "Oppers", + familyName = "Aalami" + ) + ) { null }, + Pair( + PersonNameComponents( + givenName = "Vishnu", + familyName = "Ravi", + ) + ) { + ImageResource.Vector(Icons.Default.Person) + }, + ) +} + +@ThemePreviews +@Composable +private fun UserProfileComposablePreview( + @PreviewParameter(UserProfileProvider::class) profileData: UserProfilePreviewData +) { + SpeziTheme(isPreview = true) { + UserProfileComposable( + name = profileData.first, + imageLoader = profileData.second, + ) + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt index 48102fe1f..e1fce918f 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import edu.stanford.spezi.core.design.views.views.layout.DescriptionGridRow @@ -21,6 +22,7 @@ fun NameFieldRow( description: StringResource, name: MutableState, component: KMutableProperty1, + modifier: Modifier = Modifier, label: @Composable () -> Unit, ) { NameFieldRow( @@ -36,6 +38,7 @@ fun NameFieldRow( name: MutableState, component: KMutableProperty1, description: @Composable () -> Unit, + modifier: Modifier = Modifier, label: @Composable () -> Unit, ) { DescriptionGridRow( @@ -53,24 +56,26 @@ fun NameFieldRow( private fun NameFieldRowPreview() { val name = remember { mutableStateOf(PersonNameComponents()) } - Column { - NameFieldRow( - name, - PersonNameComponents::givenName, - description = { Text("First") } - ) { - Text("enter first name") - } + SpeziTheme(isPreview = true) { + Column { + NameFieldRow( + name, + PersonNameComponents::givenName, + description = { Text("First") } + ) { + Text("enter first name") + } - HorizontalDivider(Modifier.padding(vertical = 15.dp)) + HorizontalDivider(Modifier.padding(vertical = 15.dp)) - // Last Name Field - NameFieldRow( - name, - PersonNameComponents::familyName, - description = { Text("Last") } - ) { - Text("enter last name") + // Last Name Field + NameFieldRow( + name, + PersonNameComponents::familyName, + description = { Text("Last") } + ) { + Text("enter last name") + } } } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt index 5ffacc1cc..ecc2a1375 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt @@ -1,5 +1,6 @@ package edu.stanford.spezi.core.design.views.personalInfo.fields +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.material3.TextField @@ -7,7 +8,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents import kotlin.reflect.KMutableProperty1 @@ -17,9 +20,10 @@ fun NameTextField( label: StringResource, name: MutableState, component: KMutableProperty1, + modifier: Modifier = Modifier, prompt: StringResource? = null, ) { - NameTextField(name, component, prompt) { + NameTextField(name, component, modifier, prompt) { Text(label.text()) } } @@ -28,6 +32,7 @@ fun NameTextField( fun NameTextField( name: MutableState, component: KMutableProperty1, + modifier: Modifier = Modifier, prompt: StringResource? = null, label: @Composable () -> Unit, ) { @@ -46,7 +51,8 @@ fun NameTextField( ), // TODO: Check if placeholder is the right fit for the prompt property here. placeholder = prompt?.let { { Text(it.text()) } }, - label = label + label = label, + modifier = modifier.fillMaxWidth() ) } @@ -55,7 +61,9 @@ fun NameTextField( private fun NameTextFieldPreview() { val name = remember { mutableStateOf(PersonNameComponents()) } - NameTextField(name, PersonNameComponents::givenName) { - Text("Enter first name") + SpeziTheme(isPreview = true) { + NameTextField(name, PersonNameComponents::givenName) { + Text("Enter first name") + } } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt index 5dec946f8..661df204c 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt @@ -1,11 +1,13 @@ package edu.stanford.spezi.core.design.views.validation import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationDebounce import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine @@ -17,6 +19,7 @@ import edu.stanford.spezi.core.design.views.validation.state.LocalCapturedValida fun Validate( predicate: Boolean, message: StringResource, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val rule = remember { @@ -28,6 +31,7 @@ fun Validate( Validate( input = if (predicate) "" else "FALSE", rules = listOf(rule), + modifier = modifier, content = content ) } @@ -37,6 +41,7 @@ fun Validate( fun Validate( input: String, rules: List, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val validationDebounce = LocalValidationDebounce.current @@ -66,7 +71,9 @@ fun Validate( .add(CapturedValidationState(engine, input, hasFocus)) CompositionLocalProvider(LocalValidationEngine provides engine) { - content() + Box(modifier) { + content() + } // TODO: onSubmit missing // TODO: focused missing } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt index a6aa86bc7..8496893d6 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRuleDefaults.kt @@ -36,11 +36,11 @@ val ValidationRule.Companion.minimalPassword: ValidationRule val ValidationRule.Companion.mediumPassword: ValidationRule get() = ValidationRule( regex = Regex(".{10,}"), - message = StringResource("VALIDATION_RULE_PASSWORD_LENGTH 10") + message = StringResource("Your password must be at least 10 characters long.") ) val ValidationRule.Companion.strongPassword: ValidationRule get() = ValidationRule( regex = Regex(".{12,}"), - message = StringResource("VALIDATION_RULE_PASSWORD_LENGTH 12") + message = StringResource("Your password must be at least 12 characters long.") ) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt index b07aabf20..d972b1ebb 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/ValidationResultsComposable.kt @@ -5,7 +5,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color +import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles +import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.core.design.views.validation.ValidationRule +import edu.stanford.spezi.core.design.views.validation.mediumPassword +import edu.stanford.spezi.core.design.views.validation.nonEmpty import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult @Composable @@ -24,3 +29,16 @@ fun ValidationResultsComposable( } } } + +@ThemePreviews +@Composable +private fun ValidationResultsComposablePreview() { + SpeziTheme(isPreview = true) { + ValidationResultsComposable( + listOf( + FailedValidationResult(ValidationRule.nonEmpty), + FailedValidationResult(ValidationRule.mediumPassword), + ) + ) + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index 6177b04be..cc9680f91 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -9,11 +9,18 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.core.design.views.validation.Validate +import edu.stanford.spezi.core.design.views.validation.ValidationRule import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine +import edu.stanford.spezi.core.design.views.validation.nonEmpty enum class TextFieldType { TEXT, SECURE @@ -23,12 +30,14 @@ enum class TextFieldType { fun VerifiableTextField( label: StringResource, text: MutableState, + modifier: Modifier = Modifier, type: TextFieldType = TextFieldType.TEXT, disableAutocorrection: Boolean = false, footer: @Composable () -> Unit = {}, ) { VerifiableTextField( text, + modifier, type, disableAutocorrection = disableAutocorrection, { Text(label.text()) }, @@ -39,14 +48,15 @@ fun VerifiableTextField( @Composable fun VerifiableTextField( text: MutableState, + modifier: Modifier = Modifier, type: TextFieldType = TextFieldType.TEXT, disableAutocorrection: Boolean = false, - label: @Composable () -> Unit, footer: @Composable () -> Unit = {}, + label: @Composable () -> Unit, ) { val validationEngine = LocalValidationEngine.current - Column { + Column(modifier) { // TODO: Check if this is really equivalent, // since iOS specifies this as a completely separate type // and there we only have this visualTransformation property @@ -59,6 +69,7 @@ fun VerifiableTextField( keyboardOptions = KeyboardOptions( autoCorrect = !disableAutocorrection ), + modifier = Modifier.fillMaxWidth(), ) } TextFieldType.SECURE -> { @@ -70,19 +81,35 @@ fun VerifiableTextField( keyboardType = KeyboardType.Password, autoCorrect = !disableAutocorrection ), - visualTransformation = PasswordVisualTransformation() + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), ) } } Row { - validationEngine?.let { - ValidationResultsComposable(it.displayedValidationResults) + ValidationResultsComposable(validationEngine?.displayedValidationResults ?: emptyList()) - Spacer(Modifier.fillMaxWidth()) - } + Spacer(Modifier.fillMaxWidth()) footer() } } } + +@ThemePreviews +@Composable +private fun VerifiableTextFieldPreview() { + val text = remember { mutableStateOf("") } + + SpeziTheme(isPreview = true) { + Validate(text.value, rules = listOf(ValidationRule.nonEmpty)) { + VerifiableTextField( + text, + footer = { Text("Some Hint") }, + ) { + Text("Password Text") + } + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt index 230ba0101..441a9e1c4 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt @@ -17,10 +17,11 @@ import edu.stanford.spezi.core.design.theme.ThemePreviews @Composable fun DescriptionGridRow( description: @Composable () -> Unit, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt index 8b4cf0b4d..71b2466ce 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt @@ -5,11 +5,19 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.views.model.ViewState @Composable -fun ViewStateAlert(state: MutableState) { +fun ViewStateAlert( + state: MutableState, + modifier: Modifier = Modifier, +) { if (state.value is ViewState.Error) { AlertDialog( title = { @@ -33,3 +41,13 @@ fun ViewStateAlert(state: MutableState) { ) } } + +@ThemePreviews +@Composable +private fun ViewStateAlertPreview() { + val state = remember { mutableStateOf(ViewState.Error(NotImplementedError())) } + + SpeziTheme(isPreview = true) { + ViewStateAlert(state) + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt index 85c0f4039..fa8a39491 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt @@ -7,8 +7,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.views.model.ViewState @Composable @@ -30,22 +34,34 @@ fun ProcessingOverlay( processingContent: @Composable () -> Unit = { CircularProgressIndicator() }, content: @Composable () -> Unit, ) { - val alpha = remember { mutableFloatStateOf(0f) } + val alpha = remember { mutableFloatStateOf(1f) } LaunchedEffect(isProcessing) { val newValue = if (isProcessing) 0f else 1f animate(1f - newValue, newValue) { value, _ -> alpha.floatValue = value } } - Box { + Box(contentAlignment = Alignment.Center) { Box(Modifier.alpha(alpha.floatValue)) { content() } if (isProcessing) { - Box(Modifier.matchParentSize()) { + Box(Modifier.matchParentSize(), contentAlignment = Alignment.Center) { processingContent() } } } } + +@ThemePreviews +@Composable +private fun ProcessingOverlayPreview() { + SpeziTheme(isPreview = true) { + ProcessingOverlay(true) { + SuspendButton(StringResource("Do something")) { + println("Did something") + } + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt index 3383cafd6..67c9d2136 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt @@ -8,6 +8,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import edu.stanford.spezi.core.design.component.Button import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.views.compositionLocal.LocalProcessingDebounceDuration import edu.stanford.spezi.core.design.views.views.model.ViewState import kotlinx.coroutines.delay @@ -78,3 +80,14 @@ fun SuspendButton( } } } + +@ThemePreviews +@Composable +private fun SuspendButtonPreview() { + val state = remember { mutableStateOf(ViewState.Idle) } + SpeziTheme(isPreview = true) { + SuspendButton(StringResource("Test Button"), state) { + throw NotImplementedError() + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt index 85df3c7c8..8f1678e8b 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/text/Markdown.kt @@ -11,6 +11,8 @@ import androidx.compose.runtime.setValue import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.component.markdown.MarkdownParser +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.views.model.ViewState import java.nio.charset.StandardCharsets @@ -80,3 +82,11 @@ fun Markdown( MarkdownComponent(it) } ?: CircularProgressIndicator() } + +@ThemePreviews +@Composable +private fun MarkdownPreview() { + SpeziTheme(isPreview = true) { + MarkdownString("This is a markdown **example**!") + } +} From d974d16aca83a596ec6dc569e20ef0c21c9334fc Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 15:43:53 -0800 Subject: [PATCH 16/51] fix imports --- .../edu/stanford/spezi/core/design/SpeziValidationTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt b/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt index b2c8200ae..fd7c4203d 100644 --- a/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt +++ b/core/design/src/test/kotlin/edu/stanford/spezi/core/design/SpeziValidationTest.kt @@ -1,9 +1,9 @@ package edu.stanford.spezi.core.design import com.google.common.truth.Truth.assertThat -import edu.stanford.spezi.core.design.validation.validation.ValidationEngineImpl -import edu.stanford.spezi.core.design.validation.validation.ValidationRule -import edu.stanford.spezi.core.design.validation.validation.nonEmpty +import edu.stanford.spezi.core.design.views.validation.ValidationEngineImpl +import edu.stanford.spezi.core.design.views.validation.ValidationRule +import edu.stanford.spezi.core.design.views.validation.nonEmpty import org.junit.Test class SpeziValidationTest { From 5143693c861f8ce5c2e52bdc99a38bb808ae821d Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 15:46:55 -0800 Subject: [PATCH 17/51] detekt --- .../core/design/views/personalInfo/UserProfileComposable.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt index acdea2f1d..e5830bc23 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt @@ -114,7 +114,7 @@ private class UserProfileProvider : PreviewParameterProvider Date: Mon, 11 Nov 2024 15:56:28 -0800 Subject: [PATCH 18/51] Fix tests --- .../validation/composables/FocusValidationRules.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt index e7a55a163..e62765447 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt @@ -9,13 +9,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.validation.validation.Validate -import edu.stanford.spezi.core.design.validation.validation.ValidationRule -import edu.stanford.spezi.core.design.validation.validation.minimalPassword -import edu.stanford.spezi.core.design.validation.validation.nonEmpty -import edu.stanford.spezi.core.design.validation.validation.state.ReceiveValidation -import edu.stanford.spezi.core.design.validation.validation.state.ValidationContext -import edu.stanford.spezi.core.design.validation.validation.views.VerifiableTextField +import edu.stanford.spezi.core.design.views.validation.Validate +import edu.stanford.spezi.core.design.views.validation.ValidationRule +import edu.stanford.spezi.core.design.views.validation.minimalPassword +import edu.stanford.spezi.core.design.views.validation.nonEmpty +import edu.stanford.spezi.core.design.views.validation.state.ReceiveValidation +import edu.stanford.spezi.core.design.views.validation.state.ValidationContext +import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField enum class Field { INPUT, NON_EMPTY_INPUT From 10a2543b6c552932d9525ffd2400d319f17e67f3 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 11 Nov 2024 16:03:48 -0800 Subject: [PATCH 19/51] fix --- .../composables/UserProfileTestComposable.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt index d2a711bfc..dac9fc3f2 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt @@ -17,21 +17,21 @@ import kotlin.time.Duration.Companion.seconds fun UserProfileTestComposable() { Column { UserProfileComposable( - Modifier.height(100.dp), PersonNameComponents( givenName = "Paul", familyName = "Schmiedmayer" - ) + ), + Modifier.height(100.dp), ) UserProfileComposable( - Modifier.height(200.dp), PersonNameComponents( givenName = "Leland", familyName = "Stanford" - ) + ), + Modifier.height(200.dp), ) { delay(0.5.seconds) - return@UserProfileComposable ImageResource.Vector(Icons.Default.Person) + ImageResource.Vector(Icons.Default.Person) } } } From 5c2d056ca93c471d8c6849aa865139f2563679f1 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 15:12:26 -0800 Subject: [PATCH 20/51] Update --- .../data/ContactDocumentToContactMapper.kt | 2 +- .../bdh/engagehf/contact/ui/ContactScreen.kt | 2 +- .../ContactDocumentToContactMapperTest.kt | 2 +- core/design/build.gradle.kts | 1 + .../composables/NameFieldsTestComposable.kt | 4 +- .../composables/UserProfileTestComposable.kt | 4 +- .../SuspendButtonTestComposable.kt | 2 +- .../core/design/component/ImageResource.kt | 22 ++-- .../component/ImageResourceComposable.kt | 54 +--------- .../personalInfo/PersonNameComponents.kt | 36 ------- .../personalinfo/PersonNameComponents.kt | 64 +++++++++++ .../UserProfileComposable.kt | 31 +++--- .../fields/NameFieldRow.kt | 41 +++---- .../fields/NameTextField.kt | 48 +++++---- .../views/validation/ValidationModifier.kt | 7 +- .../design/views/validation/ValidationRule.kt | 17 +-- .../ValidationDebounceDuration.kt | 8 -- .../configuration/ValidationEngine.kt | 2 +- .../ValidationEngineConfiguration.kt | 3 + .../state/CapturedValidationState.kt | 2 - .../state/FailedValidationResult.kt | 8 +- .../validation/state/ValidationContext.kt | 1 - .../validation/views/VerifiableTextField.kt | 100 +++++++++++------- .../ProcessingDebounceDuration.kt | 6 -- .../views/views/layout/DescriptionGridRow.kt | 48 +++++---- .../design/views/views/model/ViewState.kt | 12 +-- .../viewState/OperationStateAlert.kt | 16 --- .../viewModifier/viewState/ViewStateMapper.kt | 14 --- .../views/views/button/ProcessingOverlay.kt | 12 +-- .../views/views/views/button/SuspendButton.kt | 32 +++--- .../views/viewstate/OperationStateAlert.kt | 37 +++++++ .../viewState => viewstate}/ViewStateAlert.kt | 32 +++--- .../views/views/viewstate/ViewStateMapper.kt | 18 ++++ .../spezi/modules/contact/ContactFactory.kt | 2 +- .../simulator/ContactComposableSimulator.kt | 2 +- .../modules/contact/ContactComposable.kt | 2 +- .../spezi/modules/contact/model/Contact.kt | 2 +- 37 files changed, 378 insertions(+), 318 deletions(-) delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/PersonNameComponents.kt rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/{personalInfo => personalinfo}/UserProfileComposable.kt (84%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/{personalInfo => personalinfo}/fields/NameFieldRow.kt (61%) rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/{personalInfo => personalinfo}/fields/NameTextField.kt (54%) delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/OperationStateAlert.kt rename core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/{viewModifier/viewState => viewstate}/ViewStateAlert.kt (66%) create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/ViewStateMapper.kt diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt index 86489e526..7da150eaa 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapper.kt @@ -2,7 +2,7 @@ package edu.stanford.bdh.engagehf.contact.data import com.google.firebase.firestore.DocumentSnapshot import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption import edu.stanford.spezi.modules.contact.model.call diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt index a1d43bb36..4cee98dd8 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt @@ -29,7 +29,7 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.notification.R import edu.stanford.spezi.modules.contact.ContactComposable import edu.stanford.spezi.modules.contact.model.Contact diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt index 46737ea6c..747201119 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/contact/data/ContactDocumentToContactMapperTest.kt @@ -3,7 +3,7 @@ package edu.stanford.bdh.engagehf.contact.data import com.google.common.truth.Truth.assertThat import com.google.firebase.firestore.DocumentSnapshot import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import io.mockk.every import io.mockk.mockk import org.junit.Test diff --git a/core/design/build.gradle.kts b/core/design/build.gradle.kts index 70ba138b6..d71ad10f8 100644 --- a/core/design/build.gradle.kts +++ b/core/design/build.gradle.kts @@ -27,4 +27,5 @@ dependencies { debugImplementation(libs.compose.ui.tooling) debugImplementation(libs.compose.ui.test.manifest) + implementation(kotlin("reflect")) } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt index 9bcf547bb..f346d50dd 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt @@ -7,8 +7,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents -import edu.stanford.spezi.core.design.views.personalInfo.fields.NameFieldRow +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.fields.NameFieldRow @Composable fun NameFieldsTestComposable() { diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt index dac9fc3f2..409ffb65a 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt @@ -8,8 +8,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import edu.stanford.spezi.core.design.component.ImageResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents -import edu.stanford.spezi.core.design.views.personalInfo.UserProfileComposable +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.UserProfileComposable import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.seconds diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt index de6bf1e3a..c2a295e6b 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt @@ -10,8 +10,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.views.model.ViewState -import edu.stanford.spezi.core.design.views.views.viewModifier.viewState.ViewStateAlert import edu.stanford.spezi.core.design.views.views.views.button.SuspendButton +import edu.stanford.spezi.core.design.views.views.viewstate.ViewStateAlert import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.milliseconds diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResource.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResource.kt index e7c0408dd..a8cc7329b 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResource.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResource.kt @@ -3,6 +3,7 @@ package edu.stanford.spezi.core.design.component import androidx.annotation.DrawableRes import androidx.compose.ui.graphics.vector.ImageVector import edu.stanford.spezi.core.utils.UUID +import java.util.UUID import javax.annotation.concurrent.Immutable /** @@ -13,12 +14,21 @@ import javax.annotation.concurrent.Immutable * @see ImageResourceComposable */ @Immutable -sealed class ImageResource { - val identifier: String = UUID().toString() +sealed interface ImageResource { + val identifier: UUID + val contentDescription: StringResource - data class Vector(val image: ImageVector) : ImageResource() + data class Vector( + val image: ImageVector, + override val contentDescription: StringResource, + ) : ImageResource { + override val identifier: UUID = UUID() + } - data class Drawable(@DrawableRes val resId: Int) : ImageResource() - - data class Remote(val url: String) : ImageResource() + data class Drawable( + @DrawableRes val resId: Int, + override val contentDescription: StringResource, + ) : ImageResource { + override val identifier: UUID = UUID() + } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResourceComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResourceComposable.kt index a833a7f51..368ce462f 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResourceComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResourceComposable.kt @@ -1,22 +1,14 @@ package edu.stanford.spezi.core.design.component -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import coil.compose.AsyncImagePainter -import coil.compose.SubcomposeAsyncImage import edu.stanford.spezi.core.design.theme.Colors import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews @@ -28,16 +20,15 @@ import edu.stanford.spezi.core.utils.extensions.imageResourceIdentifier @Composable fun ImageResourceComposable( imageResource: ImageResource, - contentDescription: String, modifier: Modifier = Modifier, tint: Color = Colors.primary, ) { - val imageModifier = modifier.then(Modifier.imageResourceIdentifier(imageResource.identifier)) + val imageModifier = modifier.then(Modifier.imageResourceIdentifier(imageResource.identifier.toString())) when (imageResource) { is ImageResource.Vector -> { Icon( imageVector = imageResource.image, - contentDescription = contentDescription, + contentDescription = imageResource.contentDescription.text(), tint = tint, modifier = imageModifier ) @@ -46,45 +37,11 @@ fun ImageResourceComposable( is ImageResource.Drawable -> { Icon( painter = painterResource(id = imageResource.resId), - contentDescription = contentDescription, + contentDescription = imageResource.contentDescription.text(), tint = tint, modifier = imageModifier, ) } - - is ImageResource.Remote -> { - SubcomposeAsyncImage( - model = imageResource.url, - modifier = modifier, - contentDescription = contentDescription, - ) { - val state = painter.state - val painter = painter - if (state is AsyncImagePainter.State.Loading) { - ShimmerEffectBox(modifier = Modifier.matchParentSize()) - } - - if (state is AsyncImagePainter.State.Error) { - Box(Modifier.matchParentSize()) { - Text("Error loading image", Modifier.align(Alignment.Center)) - } - } - - if (state is AsyncImagePainter.State.Success) { - Box( - modifier = Modifier.matchParentSize(), - contentAlignment = Alignment.Center - ) { - Image( - modifier = Modifier.fillMaxSize(), - painter = painter, - contentDescription = contentDescription, - contentScale = ContentScale.Crop, - ) - } - } - } - } } } @@ -96,14 +53,13 @@ private fun ImageResourceComposablePreview( SpeziTheme(isPreview = true) { ImageResourceComposable( imageResource = imageResource, - contentDescription = "Icon" ) } } private class ImageResourceProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( - ImageResource.Vector(Icons.Default.ThumbUp), - ImageResource.Drawable(android.R.drawable.ic_menu_camera), + ImageResource.Vector(Icons.Default.ThumbUp, StringResource("Thumbs Up")), + ImageResource.Drawable(android.R.drawable.ic_menu_camera, StringResource("Camera")), ) } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt deleted file mode 100644 index c87fe5045..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt +++ /dev/null @@ -1,36 +0,0 @@ -package edu.stanford.spezi.core.design.views.personalInfo - -data class PersonNameComponents( - var namePrefix: String? = null, - var givenName: String? = null, - var middleName: String? = null, - var familyName: String? = null, - var nameSuffix: String? = null, - var nickname: String? = null, -) { - enum class FormatStyle { - ABBREVIATED, SHORT, MEDIUM, LONG - } - - fun formatted(style: FormatStyle = FormatStyle.LONG): String { - return when (style) { - FormatStyle.LONG -> listOfNotNull( - namePrefix, - givenName, - nickname?.let { "\"$it\"" }, - middleName, - familyName, - nameSuffix - ).joinToString(" ") - FormatStyle.MEDIUM -> - TODO("Not yet implemented.") - FormatStyle.SHORT -> - TODO("Not yet implemented.") - FormatStyle.ABBREVIATED -> listOfNotNull( - givenName, - familyName, - ).joinToString("") - .filter { it.isUpperCase() } - } - } -} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/PersonNameComponents.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/PersonNameComponents.kt new file mode 100644 index 000000000..94e55e2a9 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/PersonNameComponents.kt @@ -0,0 +1,64 @@ +package edu.stanford.spezi.core.design.views.personalinfo + +data class PersonNameComponents( + val namePrefix: String? = null, + val givenName: String? = null, + val middleName: String? = null, + val familyName: String? = null, + val nameSuffix: String? = null, + val nickname: String? = null, +) { + constructor(builder: Builder) : this( + namePrefix = builder.namePrefix, + givenName = builder.givenName, + middleName = builder.middleName, + familyName = builder.familyName, + nameSuffix = builder.nameSuffix, + nickname = builder.nickname, + ) + + enum class FormatStyle { + // TODO: Styles SHORT and MEDIUM missing + ABBREVIATED, LONG + } + + fun createBuilder() = Builder(this) + + fun formatted(style: FormatStyle = FormatStyle.LONG): String { + return when (style) { + FormatStyle.LONG -> listOfNotNull( + namePrefix, + givenName, + nickname?.let { "\"$it\"" }, + middleName, + familyName, + nameSuffix + ).joinToString(" ") + FormatStyle.ABBREVIATED -> listOfNotNull( + givenName, + familyName, + ).joinToString("") + .filter { it.isUpperCase() } + } + } + + class Builder( + var namePrefix: String? = null, + var givenName: String? = null, + var middleName: String? = null, + var familyName: String? = null, + var nameSuffix: String? = null, + var nickname: String? = null, + ) { + constructor(components: PersonNameComponents) : this( + namePrefix = components.namePrefix, + givenName = components.givenName, + middleName = components.middleName, + familyName = components.familyName, + nameSuffix = components.nameSuffix, + nickname = components.nickname, + ) + + fun build() = PersonNameComponents(this) + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/UserProfileComposable.kt similarity index 84% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/UserProfileComposable.kt index e5830bc23..60696d207 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/UserProfileComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/UserProfileComposable.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.personalInfo +package edu.stanford.spezi.core.design.views.personalinfo import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.ImageResourceComposable +import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.Colors import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews @@ -40,7 +41,7 @@ fun UserProfileComposable( imageLoader: suspend () -> ImageResource? = { null }, ) { var size by remember { mutableStateOf(IntSize.Zero) } - var loadedImage by remember { mutableStateOf(null) } + var loadedImage by remember(imageLoader) { mutableStateOf(null) } LaunchedEffect(Unit) { loadedImage = runCatching { imageLoader() } @@ -61,23 +62,20 @@ fun UserProfileComposable( loadedImage?.let { ImageResourceComposable( it, - "", // TODO: Add contentDescription to ImageResource directly? Modifier .fillMaxSize() .clip(CircleShape) .background(Colors.background, CircleShape) ) - } ?: run { - Box( - Modifier - .background(Colors.secondary, CircleShape) - .fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - formattedName, - fontSize = (sideLength.value * 0.2).sp, - color = Colors.secondary.lighten(), - ) - } + } ?: Box( + Modifier + .background(Colors.secondary, CircleShape) + .fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + formattedName, + fontSize = (sideLength.value * 0.2).sp, + color = Colors.secondary.lighten(), + ) } } } @@ -106,7 +104,10 @@ private class UserProfileProvider : PreviewParameterProvider, - component: KMutableProperty1, + description: String, + builder: PersonNameComponents.Builder, + component: KMutableProperty1, modifier: Modifier = Modifier, label: @Composable () -> Unit, ) { NameFieldRow( - name = name, + builder = builder, component = component, - description = { Text(description.text()) }, + description = { Text(description) }, + modifier = modifier, label = label ) } @Composable fun NameFieldRow( - name: MutableState, - component: KMutableProperty1, - description: @Composable () -> Unit, + builder: PersonNameComponents.Builder, + component: KMutableProperty1, + description: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, label: @Composable () -> Unit, ) { DescriptionGridRow( description = description, + modifier = modifier, content = { - NameTextField(name, component) { + NameTextField( + builder = builder, + component = component + ) { label() } } @@ -54,13 +57,13 @@ fun NameFieldRow( @ThemePreviews @Composable private fun NameFieldRowPreview() { - val name = remember { mutableStateOf(PersonNameComponents()) } + val nameBuilder = remember { PersonNameComponents.Builder() } SpeziTheme(isPreview = true) { Column { NameFieldRow( - name, - PersonNameComponents::givenName, + nameBuilder, + PersonNameComponents.Builder::givenName, description = { Text("First") } ) { Text("enter first name") @@ -70,8 +73,8 @@ private fun NameFieldRowPreview() { // Last Name Field NameFieldRow( - name, - PersonNameComponents::familyName, + nameBuilder, + PersonNameComponents.Builder::familyName, description = { Text("Last") } ) { Text("enter last name") diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt similarity index 54% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt index ecc2a1375..9f68568d9 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt @@ -1,56 +1,60 @@ -package edu.stanford.spezi.core.design.views.personalInfo.fields +package edu.stanford.spezi.core.design.views.personalinfo.fields import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import kotlin.reflect.KMutableProperty1 @Composable fun NameTextField( - label: StringResource, - name: MutableState, - component: KMutableProperty1, + label: String, + builder: PersonNameComponents.Builder, + component: KMutableProperty1, modifier: Modifier = Modifier, - prompt: StringResource? = null, + prompt: String? = null, ) { - NameTextField(name, component, modifier, prompt) { - Text(label.text()) + NameTextField( + builder = builder, + component = component, + modifier = modifier, + prompt = prompt + ) { + Text(label) } } @Composable fun NameTextField( - name: MutableState, - component: KMutableProperty1, + builder: PersonNameComponents.Builder, + component: KMutableProperty1, modifier: Modifier = Modifier, - prompt: StringResource? = null, + prompt: String? = null, label: @Composable () -> Unit, ) { + val textState = remember(builder) { + mutableStateOf(component.get(builder) ?: "") + } + // TODO: Figure out which other options to set on the keyboard for names TextField( - component.get(name.value) ?: "", + textState.value, onValueChange = { - if (it.isBlank()) { - component.set(name.value, null) - } else { - component.set(name.value, it) - } + component.set(builder, it.ifBlank { null }) + textState.value = it }, keyboardOptions = KeyboardOptions( autoCorrect = false, ), // TODO: Check if placeholder is the right fit for the prompt property here. - placeholder = prompt?.let { { Text(it.text()) } }, + placeholder = prompt?.let { { Text(it) } }, label = label, modifier = modifier.fillMaxWidth() ) @@ -59,10 +63,10 @@ fun NameTextField( @ThemePreviews @Composable private fun NameTextFieldPreview() { - val name = remember { mutableStateOf(PersonNameComponents()) } + val name = remember { PersonNameComponents.Builder() } SpeziTheme(isPreview = true) { - NameTextField(name, PersonNameComponents::givenName) { + NameTextField(name, PersonNameComponents.Builder::givenName) { Text("Enter first name") } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt index 661df204c..5ee5122c3 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt @@ -1,6 +1,5 @@ package edu.stanford.spezi.core.design.views.validation -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -9,11 +8,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationDebounce +import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngineConfiguration import edu.stanford.spezi.core.design.views.validation.state.CapturedValidationState import edu.stanford.spezi.core.design.views.validation.state.LocalCapturedValidationStateEntries +import kotlin.time.Duration @Composable fun Validate( @@ -36,15 +36,14 @@ fun Validate( ) } -@SuppressLint("MutableCollectionMutableState") // TODO: Get rid of this @Composable fun Validate( input: String, rules: List, modifier: Modifier = Modifier, + validationDebounce: Duration = DEFAULT_VALIDATION_DEBOUNCE_DURATION, content: @Composable () -> Unit, ) { - val validationDebounce = LocalValidationDebounce.current val validationEngineConfiguration = LocalValidationEngineConfiguration.current val engine = remember { ValidationEngineImpl( diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt index a4f61f9f1..28e6655c7 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationRule.kt @@ -11,12 +11,15 @@ data class ValidationRule internal constructor( val message: StringResource, val effect: CascadingValidationEffect = CascadingValidationEffect.CONTINUE, ) { - companion object { - operator fun invoke(regex: Regex, message: StringResource): ValidationRule = - ValidationRule(rule = { regex.matchEntire(it) != null }, message = message) - operator fun invoke(copy: ValidationRule, message: StringResource): ValidationRule = - ValidationRule(rule = copy.rule, message = message) - } + constructor(regex: Regex, message: StringResource) : this( + rule = { regex.matchEntire(it) != null }, + message = message, + ) + + constructor(copy: ValidationRule, message: StringResource) : this( + rule = copy.rule, + message = message, + ) val intercepting: ValidationRule get() = ValidationRule(id, rule, message, CascadingValidationEffect.INTERCEPT) @@ -28,4 +31,6 @@ data class ValidationRule internal constructor( if (rule(input)) null else FailedValidationResult(this) override fun hashCode(): Int = id.hashCode() + + companion object } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt deleted file mode 100644 index 15cef5d76..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationDebounceDuration.kt +++ /dev/null @@ -1,8 +0,0 @@ -package edu.stanford.spezi.core.design.views.validation.configuration - -import androidx.compose.runtime.compositionLocalOf -import kotlin.time.Duration.Companion.seconds - -internal val DEFAULT_VALIDATION_DEBOUNCE_DURATION = 0.5.seconds - -val LocalValidationDebounce = compositionLocalOf { DEFAULT_VALIDATION_DEBOUNCE_DURATION } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt index f02fdc40a..134d18aae 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngine.kt @@ -3,4 +3,4 @@ package edu.stanford.spezi.core.design.views.validation.configuration import androidx.compose.runtime.compositionLocalOf import edu.stanford.spezi.core.design.views.validation.ValidationEngine -val LocalValidationEngine = compositionLocalOf { null } +internal val LocalValidationEngine = compositionLocalOf { null } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt index 30b193192..b75558ad0 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/configuration/ValidationEngineConfiguration.kt @@ -3,6 +3,9 @@ package edu.stanford.spezi.core.design.views.validation.configuration import androidx.compose.runtime.compositionLocalOf import edu.stanford.spezi.core.design.views.validation.ValidationEngine import edu.stanford.spezi.core.design.views.validation.ValidationEngineConfiguration +import kotlin.time.Duration.Companion.milliseconds + +internal val DEFAULT_VALIDATION_DEBOUNCE_DURATION = 150.milliseconds val LocalValidationEngineConfiguration = compositionLocalOf { ValidationEngineConfiguration.noneOf(ValidationEngine.ConfigurationOption::class.java) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt index 5288bb5f7..2b20999df 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/CapturedValidationState.kt @@ -16,8 +16,6 @@ data class CapturedValidationState internal constructor( engine.runValidation(input) } - // TODO: Find out whether we can dynamically expose members of ValidationEngine - override fun hashCode(): Int { return super.hashCode() } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt index f46992f52..3ed8afa68 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/FailedValidationResult.kt @@ -8,10 +8,10 @@ data class FailedValidationResult( val id: UUID, val message: StringResource, ) { - companion object { - operator fun invoke(rule: ValidationRule) = - FailedValidationResult(rule.id, rule.message) - } + constructor(rule: ValidationRule) : this( + id = rule.id, + message = rule.message + ) override fun equals(other: Any?) = (other as? FailedValidationResult)?.id == id override fun hashCode() = id.hashCode() diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt index a12a71d86..fea465d49 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/state/ValidationContext.kt @@ -28,7 +28,6 @@ data class ValidationContext internal constructor( } } - // TODO: Originally called validateSubviews, but renamed to avoid using "view" on Android fun validateHierarchy(switchFocus: Boolean = true): Boolean { val failedFields = collectFailedValidations() diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index cc9680f91..7f62913b2 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews @@ -29,25 +30,68 @@ enum class TextFieldType { @Composable fun VerifiableTextField( label: StringResource, - text: MutableState, + state: MutableState, modifier: Modifier = Modifier, type: TextFieldType = TextFieldType.TEXT, disableAutocorrection: Boolean = false, footer: @Composable () -> Unit = {}, ) { VerifiableTextField( - text, - modifier, - type, + value = state.value, + onValueChanged = { state.value = it }, + modifier = modifier, + type = type, disableAutocorrection = disableAutocorrection, - { Text(label.text()) }, - footer + footer = footer, + label = { Text(label.text()) }, ) } @Composable fun VerifiableTextField( - text: MutableState, + label: StringResource, + value: String, + onValueChanged: (String) -> Unit, + modifier: Modifier = Modifier, + type: TextFieldType = TextFieldType.TEXT, + disableAutocorrection: Boolean = false, + footer: @Composable () -> Unit = {}, +) { + VerifiableTextField( + value = value, + onValueChanged = onValueChanged, + modifier = modifier, + type = type, + disableAutocorrection = disableAutocorrection, + footer = footer, + label = { Text(label.text()) }, + ) +} + +@Composable +fun VerifiableTextField( + state: MutableState, + modifier: Modifier = Modifier, + type: TextFieldType = TextFieldType.TEXT, + disableAutocorrection: Boolean = false, + footer: @Composable () -> Unit = {}, + label: @Composable () -> Unit, +) { + VerifiableTextField( + value = state.value, + onValueChanged = { state.value = it }, + modifier = modifier, + type = type, + disableAutocorrection = disableAutocorrection, + footer = footer, + label = label + ) +} + +@Composable +fun VerifiableTextField( + value: String, + onValueChanged: (String) -> Unit, modifier: Modifier = Modifier, type: TextFieldType = TextFieldType.TEXT, disableAutocorrection: Boolean = false, @@ -55,37 +99,21 @@ fun VerifiableTextField( label: @Composable () -> Unit, ) { val validationEngine = LocalValidationEngine.current + val isSecure = remember(type) { type == TextFieldType.SECURE } Column(modifier) { - // TODO: Check if this is really equivalent, - // since iOS specifies this as a completely separate type - // and there we only have this visualTransformation property - when (type) { - TextFieldType.TEXT -> { - TextField( - text.value, - onValueChange = { text.value = it }, - label = label, - keyboardOptions = KeyboardOptions( - autoCorrect = !disableAutocorrection - ), - modifier = Modifier.fillMaxWidth(), - ) - } - TextFieldType.SECURE -> { - TextField( - text.value, - onValueChange = { text.value = it }, - label = label, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - autoCorrect = !disableAutocorrection - ), - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth(), - ) - } - } + // TODO: Check equality with iOS + TextField( + value = value, + onValueChange = onValueChanged, + label = label, + keyboardOptions = KeyboardOptions( + keyboardType = if (isSecure) KeyboardType.Password else KeyboardType.Text, + autoCorrect = !disableAutocorrection + ), + visualTransformation = if (isSecure) PasswordVisualTransformation() else VisualTransformation.None, + modifier = Modifier.fillMaxWidth(), + ) Row { ValidationResultsComposable(validationEngine?.displayedValidationResults ?: emptyList()) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt deleted file mode 100644 index 2c96fa2a8..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/compositionLocal/ProcessingDebounceDuration.kt +++ /dev/null @@ -1,6 +0,0 @@ -package edu.stanford.spezi.core.design.views.views.compositionLocal - -import androidx.compose.runtime.compositionLocalOf -import kotlin.time.Duration.Companion.milliseconds - -val LocalProcessingDebounceDuration = compositionLocalOf { 150.milliseconds } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt index 441a9e1c4..bc6657856 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt @@ -2,6 +2,7 @@ package edu.stanford.spezi.core.design.views.views.layout import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -12,13 +13,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews +private const val DESCRIPTION_GRID_ROW_COMPONENT_WIDTH_FACTOR = 0.5f + @Composable fun DescriptionGridRow( - description: @Composable () -> Unit, + description: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, - content: @Composable () -> Unit, + content: @Composable BoxScope.() -> Unit, ) { Row( modifier = modifier @@ -30,7 +34,7 @@ fun DescriptionGridRow( Box( modifier = Modifier .alignByBaseline() - .weight(1f, fill = false) + .weight(DESCRIPTION_GRID_ROW_COMPONENT_WIDTH_FACTOR, fill = false) ) { description() } @@ -39,7 +43,7 @@ fun DescriptionGridRow( modifier = Modifier .alignByBaseline() .fillMaxWidth() - .weight(1f) + .weight(DESCRIPTION_GRID_ROW_COMPONENT_WIDTH_FACTOR) ) { content() } @@ -49,27 +53,29 @@ fun DescriptionGridRow( @ThemePreviews @Composable private fun DescriptionGridRowPreviews() { - Column { - DescriptionGridRow(description = { - Text("Description") - }) { - Text("Content") - } + SpeziTheme(isPreview = true) { + Column { + DescriptionGridRow(description = { + Text("Description") + }) { + Text("Content") + } - HorizontalDivider() + HorizontalDivider() - DescriptionGridRow(description = { - Text("Description") - }) { - Text("Content") - } + DescriptionGridRow(description = { + Text("Description") + }) { + Text("Content") + } - HorizontalDivider() + HorizontalDivider() - DescriptionGridRow(description = { - Text("Description") - }) { - Text("Content") + DescriptionGridRow(description = { + Text("Description") + }) { + Text("Content") + } } } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt index a73b14109..c5c798033 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/model/ViewState.kt @@ -7,11 +7,11 @@ import edu.stanford.spezi.core.design.component.StringResource sealed interface ViewState { data object Idle : ViewState data object Processing : ViewState - data class Error(val throwable: Throwable?) : ViewState + data class Error(val throwable: Throwable?) : ViewState { + val errorTitle: String + @Composable @ReadOnlyComposable get() = StringResource("Error").text() - val errorTitle: String - @Composable @ReadOnlyComposable get() = StringResource("Error").text() - - val errorDescription: String - @Composable @ReadOnlyComposable get() = if (this is Error) throwable?.localizedMessage ?: "" else "" + val errorDescription: String + @Composable @ReadOnlyComposable get() = throwable?.localizedMessage ?: "" + } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt deleted file mode 100644 index cb2eb4abb..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/OperationStateAlert.kt +++ /dev/null @@ -1,16 +0,0 @@ -package edu.stanford.spezi.core.design.views.views.viewModifier.viewState - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import edu.stanford.spezi.core.design.views.views.model.OperationState - -@Composable -fun OperationStateAlert( - state: MutableState, -) { - val viewState = remember { mutableStateOf(state.value.representation) } - MapOperationStateToViewState(state.value, viewState) - ViewStateAlert(viewState) -} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt deleted file mode 100644 index 1ca174b1d..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateMapper.kt +++ /dev/null @@ -1,14 +0,0 @@ -package edu.stanford.spezi.core.design.views.views.viewModifier.viewState - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import edu.stanford.spezi.core.design.views.views.model.OperationState -import edu.stanford.spezi.core.design.views.views.model.ViewState - -@Composable -fun MapOperationStateToViewState(state: State, viewState: MutableState) { - LaunchedEffect(state) { - viewState.value = state.representation - } -} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt index fa8a39491..f4242860e 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt @@ -2,6 +2,7 @@ package edu.stanford.spezi.core.design.views.views.views.button import androidx.compose.animation.core.animate import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -10,7 +11,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.views.model.ViewState @@ -18,8 +18,8 @@ import edu.stanford.spezi.core.design.views.views.model.ViewState @Composable fun ProcessingOverlay( viewState: ViewState, - processingContent: @Composable () -> Unit = { CircularProgressIndicator() }, - content: @Composable () -> Unit, + processingContent: @Composable BoxScope.() -> Unit = { CircularProgressIndicator() }, + content: @Composable BoxScope.() -> Unit, ) { ProcessingOverlay( isProcessing = viewState == ViewState.Processing, @@ -31,8 +31,8 @@ fun ProcessingOverlay( @Composable fun ProcessingOverlay( isProcessing: Boolean, - processingContent: @Composable () -> Unit = { CircularProgressIndicator() }, - content: @Composable () -> Unit, + processingContent: @Composable BoxScope.() -> Unit = { CircularProgressIndicator() }, + content: @Composable BoxScope.() -> Unit, ) { val alpha = remember { mutableFloatStateOf(1f) } LaunchedEffect(isProcessing) { @@ -59,7 +59,7 @@ fun ProcessingOverlay( private fun ProcessingOverlayPreview() { SpeziTheme(isPreview = true) { ProcessingOverlay(true) { - SuspendButton(StringResource("Do something")) { + SuspendButton("Do something") { println("Did something") } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt index 67c9d2136..e60792bf5 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/SuspendButton.kt @@ -7,14 +7,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import edu.stanford.spezi.core.design.component.Button -import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.views.views.compositionLocal.LocalProcessingDebounceDuration import edu.stanford.spezi.core.design.views.views.model.ViewState import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds private enum class SuspendButtonState { IDLE, DISABLED, DISABLED_AND_PROCESSING @@ -22,25 +21,25 @@ private enum class SuspendButtonState { @Composable fun SuspendButton( - title: StringResource, + title: String, state: MutableState = remember { mutableStateOf(ViewState.Idle) }, action: suspend () -> Unit, ) { - SuspendButton(state, action) { - Text(title.text()) + SuspendButton(state = state, action = action) { + Text(title) } } @Composable fun SuspendButton( + processingDebounceDuration: Duration = 150.milliseconds, state: MutableState = remember { mutableStateOf(ViewState.Idle) }, action: suspend () -> Unit, label: @Composable () -> Unit, ) { val buttonState = remember { mutableStateOf(SuspendButtonState.IDLE) } val coroutineScope = rememberCoroutineScope() - val debounceScope = rememberCoroutineScope() - val processingDebounceDuration = LocalProcessingDebounceDuration.current + val debounceIsCancelled = remember { mutableStateOf(false) } val externallyProcessing = buttonState.value == SuspendButtonState.IDLE && state.value == ViewState.Processing Button( @@ -49,25 +48,24 @@ fun SuspendButton( if (state.value == ViewState.Processing) return@Button buttonState.value = SuspendButtonState.DISABLED - val debounceJob = debounceScope.launch { + coroutineScope.launch { delay(processingDebounceDuration) - if (isActive) { - buttonState.value = SuspendButtonState.DISABLED_AND_PROCESSING - } + if (debounceIsCancelled.value) return@launch + buttonState.value = SuspendButtonState.DISABLED_AND_PROCESSING } state.value = ViewState.Processing coroutineScope.launch { runCatching { action() - debounceJob.cancel() + debounceIsCancelled.value = true if (state.value != ViewState.Idle) { state.value = ViewState.Idle } }.onFailure { - debounceJob.cancel() + debounceIsCancelled.value = true state.value = ViewState.Error(it) } @@ -75,7 +73,9 @@ fun SuspendButton( } }, ) { - ProcessingOverlay(buttonState.value == SuspendButtonState.DISABLED_AND_PROCESSING || externallyProcessing) { + ProcessingOverlay( + isProcessing = buttonState.value == SuspendButtonState.DISABLED_AND_PROCESSING || externallyProcessing + ) { label() } } @@ -86,7 +86,7 @@ fun SuspendButton( private fun SuspendButtonPreview() { val state = remember { mutableStateOf(ViewState.Idle) } SpeziTheme(isPreview = true) { - SuspendButton(StringResource("Test Button"), state) { + SuspendButton("Test Button", state) { throw NotImplementedError() } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/OperationStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/OperationStateAlert.kt new file mode 100644 index 000000000..30d1e6275 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/OperationStateAlert.kt @@ -0,0 +1,37 @@ +package edu.stanford.spezi.core.design.views.views.viewstate + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import edu.stanford.spezi.core.design.views.views.model.OperationState +import edu.stanford.spezi.core.design.views.views.model.ViewState + +@Composable +fun OperationStateAlert( + state: MutableState, + modifier: Modifier = Modifier, + onClose: () -> Unit = {}, +) { + val viewState = mapOperationStateToViewState(state.value) + ViewStateAlert( + state = viewState.value, + modifier = modifier, + onClose = { + viewState.value = ViewState.Idle + onClose() + } + ) +} + +@Composable +fun OperationStateAlert( + state: State, + modifier: Modifier = Modifier, + onClose: () -> Unit, +) { + ViewStateAlert( + state = state.representation, + modifier = modifier, + onClose = onClose + ) +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/ViewStateAlert.kt similarity index 66% rename from core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt rename to core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/ViewStateAlert.kt index 71b2466ce..0c8f2d44c 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewModifier/viewState/ViewStateAlert.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/ViewStateAlert.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.core.design.views.views.viewModifier.viewState +package edu.stanford.spezi.core.design.views.views.viewstate import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text @@ -18,23 +18,31 @@ fun ViewStateAlert( state: MutableState, modifier: Modifier = Modifier, ) { - if (state.value is ViewState.Error) { + ViewStateAlert( + state = state.value, + onClose = { state.value = ViewState.Idle }, + modifier = modifier + ) +} + +@Composable +fun ViewStateAlert( + state: ViewState, + modifier: Modifier = Modifier, + onClose: () -> Unit, +) { + if (state is ViewState.Error) { AlertDialog( + modifier = modifier, title = { - Text(text = state.value.errorTitle) + Text(text = state.errorTitle) }, text = { - Text(text = state.value.errorDescription) - }, - onDismissRequest = { - state.value = ViewState.Idle + Text(text = state.errorDescription) }, + onDismissRequest = onClose, confirmButton = { - TextButton( - onClick = { - state.value = ViewState.Idle - } - ) { + TextButton(onClick = onClose) { Text(StringResource("OK").text()) } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/ViewStateMapper.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/ViewStateMapper.kt new file mode 100644 index 000000000..a2b3b3954 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/viewstate/ViewStateMapper.kt @@ -0,0 +1,18 @@ +package edu.stanford.spezi.core.design.views.views.viewstate + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.core.design.views.views.model.OperationState +import edu.stanford.spezi.core.design.views.views.model.ViewState + +@Composable +fun mapOperationStateToViewState(state: State): MutableState { + val result = remember { mutableStateOf(state.representation) } + LaunchedEffect(state) { + result.value = state.representation + } + return result +} diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt index 89630f527..7c0a17be2 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt @@ -5,7 +5,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption import edu.stanford.spezi.modules.contact.model.call diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt index 7db3a40cc..b3c48d390 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.test.platform.app.InstrumentationRegistry import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.testing.assertImageIdentifier import edu.stanford.spezi.core.testing.onNodeWithIdentifier import edu.stanford.spezi.modules.contact.ContactComposableTestIdentifier diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt index 9bede632d..f76fcb2c0 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt @@ -32,7 +32,7 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles import edu.stanford.spezi.core.design.theme.ThemePreviews -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.utils.extensions.testIdentifier import edu.stanford.spezi.modules.contact.component.AddressCard import edu.stanford.spezi.modules.contact.component.ContactOptionCard diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt index c151b35b5..92a2d3fd5 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt @@ -5,7 +5,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import edu.stanford.spezi.core.design.component.ImageResource import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import java.util.UUID /** From 6f5b66cec84fcfff73206c8804d18b61af60eb30 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 15:29:29 -0800 Subject: [PATCH 21/51] review adaptions --- .../design/views/personalinfo/fields/NameFieldRow.kt | 7 +++---- .../design/views/personalinfo/fields/NameTextField.kt | 8 ++------ .../design/views/views/layout/DescriptionGridRow.kt | 10 +++------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameFieldRow.kt index 042376a79..69f62d4cf 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameFieldRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameFieldRow.kt @@ -46,10 +46,9 @@ fun NameFieldRow( content = { NameTextField( builder = builder, - component = component - ) { - label() - } + component = component, + label = label, + ) } ) } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt index 9f68568d9..9dce3c5e4 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt @@ -19,24 +19,22 @@ fun NameTextField( builder: PersonNameComponents.Builder, component: KMutableProperty1, modifier: Modifier = Modifier, - prompt: String? = null, ) { NameTextField( builder = builder, component = component, modifier = modifier, - prompt = prompt ) { Text(label) } } +// TODO: We got rid of "prompt" property here @Composable fun NameTextField( builder: PersonNameComponents.Builder, component: KMutableProperty1, modifier: Modifier = Modifier, - prompt: String? = null, label: @Composable () -> Unit, ) { val textState = remember(builder) { @@ -53,9 +51,7 @@ fun NameTextField( keyboardOptions = KeyboardOptions( autoCorrect = false, ), - // TODO: Check if placeholder is the right fit for the prompt property here. - placeholder = prompt?.let { { Text(it) } }, - label = label, + placeholder = label, modifier = modifier.fillMaxWidth() ) } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt index bc6657856..1b9d458b2 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/layout/DescriptionGridRow.kt @@ -12,12 +12,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews -private const val DESCRIPTION_GRID_ROW_COMPONENT_WIDTH_FACTOR = 0.5f - @Composable fun DescriptionGridRow( description: @Composable BoxScope.() -> Unit, @@ -27,14 +25,13 @@ fun DescriptionGridRow( Row( modifier = modifier .fillMaxWidth() - .padding(vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + .padding(vertical = Spacings.small), + horizontalArrangement = Arrangement.spacedBy(Spacings.medium), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .alignByBaseline() - .weight(DESCRIPTION_GRID_ROW_COMPONENT_WIDTH_FACTOR, fill = false) ) { description() } @@ -43,7 +40,6 @@ fun DescriptionGridRow( modifier = Modifier .alignByBaseline() .fillMaxWidth() - .weight(DESCRIPTION_GRID_ROW_COMPONENT_WIDTH_FACTOR) ) { content() } From e02bad3c70959f9a7406fbb889e9fa8cde7fc20c Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 15:37:41 -0800 Subject: [PATCH 22/51] tests --- .../design/personalInfo/NameFieldsTest.kt | 6 ++++-- .../composables/NameFieldsTestComposable.kt | 6 +++--- .../composables/UserProfileTestComposable.kt | 3 ++- .../simulators/NameFieldsTestSimulator.kt | 9 ++++++--- .../SuspendButtonTestComposable.kt | 4 ++-- .../views/personalinfo/fields/NameFieldRow.kt | 8 ++++---- .../personalinfo/fields/NameTextField.kt | 19 +++++++++++++------ 7 files changed, 34 insertions(+), 21 deletions(-) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt index 4e339adce..d2818d337 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt @@ -1,8 +1,10 @@ package edu.stanford.spezi.core.design.personalInfo +import android.app.Person import androidx.compose.ui.test.junit4.createComposeRule import edu.stanford.spezi.core.design.personalInfo.composables.NameFieldsTestComposable import edu.stanford.spezi.core.design.personalInfo.simulators.NameFieldsTestSimulator +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import org.junit.Before import org.junit.Rule import org.junit.Test @@ -25,8 +27,8 @@ class NameFieldsTest { assertTextExists("First Name") assertTextExists("Last Name") - enterText("enter your first name", "Leland") - enterText("enter your last name", "Stanford") + enterText(PersonNameComponents.Builder::givenName, "Leland") + enterText(PersonNameComponents.Builder::familyName, "Stanford") } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt index f346d50dd..bdb34a573 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt @@ -12,16 +12,16 @@ import edu.stanford.spezi.core.design.views.personalinfo.fields.NameFieldRow @Composable fun NameFieldsTestComposable() { - val name = remember { mutableStateOf(PersonNameComponents()) } + val nameBuilder = remember { PersonNameComponents.Builder() } Column { - NameFieldRow(StringResource("First Name"), name, PersonNameComponents::givenName) { + NameFieldRow("First Name", nameBuilder, PersonNameComponents.Builder::givenName) { Text("enter your first name") } HorizontalDivider() - NameFieldRow(StringResource("Last Name"), name, PersonNameComponents::familyName) { + NameFieldRow("Last Name", nameBuilder, PersonNameComponents.Builder::familyName) { Text("enter your last name") } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt index 409ffb65a..6e51756ef 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/UserProfileTestComposable.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import edu.stanford.spezi.core.design.component.ImageResource +import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.personalinfo.UserProfileComposable import kotlinx.coroutines.delay @@ -31,7 +32,7 @@ fun UserProfileTestComposable() { Modifier.height(200.dp), ) { delay(0.5.seconds) - ImageResource.Vector(Icons.Default.Person) + ImageResource.Vector(Icons.Default.Person, StringResource("Person")) } } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/NameFieldsTestSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/NameFieldsTestSimulator.kt index c71708838..9c671de20 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/NameFieldsTestSimulator.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/NameFieldsTestSimulator.kt @@ -3,20 +3,23 @@ package edu.stanford.spezi.core.design.personalInfo.simulators import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalinfo.fields.NameTextFieldTestIdentifier +import edu.stanford.spezi.core.testing.onNodeWithIdentifier +import kotlin.reflect.KMutableProperty1 class NameFieldsTestSimulator( private val composeTestRule: ComposeTestRule, ) { - fun assertTextExists(text: String) { composeTestRule .onNodeWithText(text) .assertExists() } - fun enterText(placeholder: String, text: String) { + fun enterText(property: KMutableProperty1, text: String) { composeTestRule - .onNodeWithText(placeholder) + .onNodeWithIdentifier(NameTextFieldTestIdentifier.TEXT_FIELD, property.name) .performTextInput(text) } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt index c2a295e6b..0912b2345 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt @@ -33,12 +33,12 @@ fun SuspendButtonTestComposable() { Text("Reset") } } else { - SuspendButton(StringResource("Hello World")) { + SuspendButton("Hello World") { delay(500.milliseconds) showCompleted = true } - SuspendButton(StringResource("Hello Throwing World"), viewState) { + SuspendButton("Hello Throwing World", viewState) { throw CustomError() } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameFieldRow.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameFieldRow.kt index 69f62d4cf..3f614b2f5 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameFieldRow.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameFieldRow.kt @@ -19,13 +19,13 @@ import kotlin.reflect.KMutableProperty1 fun NameFieldRow( description: String, builder: PersonNameComponents.Builder, - component: KMutableProperty1, + property: KMutableProperty1, modifier: Modifier = Modifier, label: @Composable () -> Unit, ) { NameFieldRow( builder = builder, - component = component, + property = property, description = { Text(description) }, modifier = modifier, label = label @@ -35,7 +35,7 @@ fun NameFieldRow( @Composable fun NameFieldRow( builder: PersonNameComponents.Builder, - component: KMutableProperty1, + property: KMutableProperty1, description: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, label: @Composable () -> Unit, @@ -46,7 +46,7 @@ fun NameFieldRow( content = { NameTextField( builder = builder, - component = component, + property = property, label = label, ) } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt index 9dce3c5e4..e9d75c02a 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalinfo/fields/NameTextField.kt @@ -11,18 +11,23 @@ import androidx.compose.ui.Modifier import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.core.utils.extensions.testIdentifier import kotlin.reflect.KMutableProperty1 +enum class NameTextFieldTestIdentifier { + TEXT_FIELD, +} + @Composable fun NameTextField( label: String, builder: PersonNameComponents.Builder, - component: KMutableProperty1, + property: KMutableProperty1, modifier: Modifier = Modifier, ) { NameTextField( builder = builder, - component = component, + property = property, modifier = modifier, ) { Text(label) @@ -33,26 +38,28 @@ fun NameTextField( @Composable fun NameTextField( builder: PersonNameComponents.Builder, - component: KMutableProperty1, + property: KMutableProperty1, modifier: Modifier = Modifier, label: @Composable () -> Unit, ) { val textState = remember(builder) { - mutableStateOf(component.get(builder) ?: "") + mutableStateOf(property.get(builder) ?: "") } // TODO: Figure out which other options to set on the keyboard for names TextField( textState.value, onValueChange = { - component.set(builder, it.ifBlank { null }) + property.set(builder, it.ifBlank { null }) textState.value = it }, keyboardOptions = KeyboardOptions( autoCorrect = false, ), placeholder = label, - modifier = modifier.fillMaxWidth() + modifier = modifier + .fillMaxWidth() + .testIdentifier(NameTextFieldTestIdentifier.TEXT_FIELD, property.name) ) } From 0a0fc2f653180d219e743b3df4e8ffd98e24040b Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 15:48:53 -0800 Subject: [PATCH 23/51] adapt tests --- .../core/design/personalInfo/NameFieldsTest.kt | 18 +++++++++++++++--- .../composables/NameFieldsTestComposable.kt | 4 +--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt index d2818d337..50132a853 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt @@ -2,6 +2,7 @@ package edu.stanford.spezi.core.design.personalInfo import android.app.Person import androidx.compose.ui.test.junit4.createComposeRule +import com.google.common.truth.Truth.assertThat import edu.stanford.spezi.core.design.personalInfo.composables.NameFieldsTestComposable import edu.stanford.spezi.core.design.personalInfo.simulators.NameFieldsTestSimulator import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents @@ -14,21 +15,32 @@ class NameFieldsTest { @get:Rule val composeTestRule = createComposeRule() + val nameBuilder = PersonNameComponents.Builder() + @Before fun init() { composeTestRule.setContent { - NameFieldsTestComposable() + NameFieldsTestComposable(nameBuilder) } } @Test fun testNameFields() { + val givenName = "Leland" + val familyName = "Stanford" + nameFields { assertTextExists("First Name") assertTextExists("Last Name") - enterText(PersonNameComponents.Builder::givenName, "Leland") - enterText(PersonNameComponents.Builder::familyName, "Stanford") + enterText(PersonNameComponents.Builder::givenName, givenName) + enterText(PersonNameComponents.Builder::familyName, familyName) + + assertTextExists("First Name") + assertTextExists("Last Name") + + assertThat(nameBuilder.givenName).isEqualTo(givenName) + assertThat(nameBuilder.familyName).isEqualTo(familyName) } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt index bdb34a573..36fdbcde6 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt @@ -11,9 +11,7 @@ import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.personalinfo.fields.NameFieldRow @Composable -fun NameFieldsTestComposable() { - val nameBuilder = remember { PersonNameComponents.Builder() } - +fun NameFieldsTestComposable(nameBuilder: PersonNameComponents.Builder) { Column { NameFieldRow("First Name", nameBuilder, PersonNameComponents.Builder::givenName) { Text("enter your first name") From 35f5076752091ac42c62ab064e5c0c1b89c7b9e8 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 16:09:05 -0800 Subject: [PATCH 24/51] update --- .../design/personalInfo/NameFieldsTest.kt | 1 - .../composables/NameFieldsTestComposable.kt | 3 - .../SuspendButtonTestComposable.kt | 1 - .../design/component/AsyncImageResource.kt | 47 ++++++++ .../component/AsyncImageResourceComposable.kt | 114 ++++++++++++++++++ .../component/ExpandableVideoSection.kt | 10 +- 6 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResource.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResourceComposable.kt diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt index 50132a853..a3feafbf7 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/NameFieldsTest.kt @@ -1,6 +1,5 @@ package edu.stanford.spezi.core.design.personalInfo -import android.app.Person import androidx.compose.ui.test.junit4.createComposeRule import com.google.common.truth.Truth.assertThat import edu.stanford.spezi.core.design.personalInfo.composables.NameFieldsTestComposable diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt index 36fdbcde6..af1212881 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/composables/NameFieldsTestComposable.kt @@ -4,9 +4,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.personalinfo.fields.NameFieldRow diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt index 0912b2345..1badf7e1d 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/views/composables/SuspendButtonTestComposable.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.views.model.ViewState import edu.stanford.spezi.core.design.views.views.views.button.SuspendButton import edu.stanford.spezi.core.design.views.views.viewstate.ViewStateAlert diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResource.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResource.kt new file mode 100644 index 000000000..6d66342f6 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResource.kt @@ -0,0 +1,47 @@ +package edu.stanford.spezi.core.design.component + +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.vector.ImageVector +import edu.stanford.spezi.core.utils.UUID +import java.util.UUID +import javax.annotation.concurrent.Immutable + +@Immutable +sealed interface AsyncImageResource { + val identifier: UUID + val contentDescription: StringResource + + data class Remote( + val url: String, + override val contentDescription: StringResource, + ) : AsyncImageResource { + override val identifier: UUID = UUID() + } + + data class Vector( + val image: ImageVector, + override val contentDescription: StringResource, + ) : AsyncImageResource { + override val identifier: UUID = UUID() + } + + data class Drawable( + @DrawableRes val resId: Int, + override val contentDescription: StringResource, + ) : AsyncImageResource { + override val identifier: UUID = UUID() + } + + companion object { + operator fun invoke(imageResource: ImageResource): AsyncImageResource = when (imageResource) { + is ImageResource.Vector -> AsyncImageResource.Vector( + image = imageResource.image, + contentDescription = imageResource.contentDescription + ) + is ImageResource.Drawable -> AsyncImageResource.Drawable( + resId = imageResource.resId, + contentDescription = imageResource.contentDescription + ) + } + } +} diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResourceComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResourceComposable.kt new file mode 100644 index 000000000..b76dce6ad --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResourceComposable.kt @@ -0,0 +1,114 @@ +package edu.stanford.spezi.core.design.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ThumbUp +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import coil.compose.AsyncImagePainter +import coil.compose.SubcomposeAsyncImage +import edu.stanford.spezi.core.design.theme.Colors +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.core.utils.extensions.imageResourceIdentifier + +/** + * Composable function to display an icon using an [ImageResource]. + */ +@Composable +fun AsyncImageResourceComposable( + imageResource: AsyncImageResource, + modifier: Modifier = Modifier, + loadingContent: @Composable BoxScope.() -> Unit = { + ShimmerEffectBox(modifier = Modifier.matchParentSize()) + }, + errorContent: @Composable BoxScope.(Throwable) -> Unit = { + Box(Modifier.matchParentSize()) { + Text("Error loading image", Modifier.align(Alignment.Center)) + } + }, + tint: Color = Colors.primary, +) { + val imageModifier = modifier.then(Modifier.imageResourceIdentifier(imageResource.identifier.toString())) + when (imageResource) { + is AsyncImageResource.Vector -> { + Icon( + imageVector = imageResource.image, + contentDescription = imageResource.contentDescription.text(), + tint = tint, + modifier = imageModifier + ) + } + + is AsyncImageResource.Drawable -> { + Icon( + painter = painterResource(id = imageResource.resId), + contentDescription = imageResource.contentDescription.text(), + tint = tint, + modifier = imageModifier, + ) + } + + is AsyncImageResource.Remote -> { + SubcomposeAsyncImage( + model = imageResource.url, + modifier = modifier, + contentDescription = imageResource.contentDescription.text(), + ) { + val state = painter.state + val painter = painter + if (state is AsyncImagePainter.State.Loading) { + loadingContent() + } + + if (state is AsyncImagePainter.State.Error) { + errorContent(state.result.throwable) + } + + if (state is AsyncImagePainter.State.Success) { + Box( + modifier = Modifier.matchParentSize(), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.fillMaxSize(), + painter = painter, + contentDescription = imageResource.contentDescription.text(), + contentScale = ContentScale.Crop, + ) + } + } + } + } + } +} + +@ThemePreviews +@Composable +private fun ImageResourceComposablePreview( + @PreviewParameter(AsyncImageResourceProvider::class) imageResource: ImageResource, +) { + SpeziTheme(isPreview = true) { + ImageResourceComposable( + imageResource = imageResource, + ) + } +} + +private class AsyncImageResourceProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ImageResource.Vector(Icons.Default.ThumbUp, StringResource("Thumbs up")), + ImageResource.Drawable(android.R.drawable.ic_menu_camera, StringResource("Camera")), + ) +} diff --git a/modules/education/src/main/java/edu/stanford/spezi/modules/education/videos/component/ExpandableVideoSection.kt b/modules/education/src/main/java/edu/stanford/spezi/modules/education/videos/component/ExpandableVideoSection.kt index dec393094..ef483dbb5 100644 --- a/modules/education/src/main/java/edu/stanford/spezi/modules/education/videos/component/ExpandableVideoSection.kt +++ b/modules/education/src/main/java/edu/stanford/spezi/modules/education/videos/component/ExpandableVideoSection.kt @@ -35,10 +35,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.component.AsyncImageResource +import edu.stanford.spezi.core.design.component.AsyncImageResourceComposable import edu.stanford.spezi.core.design.component.DefaultElevatedCard -import edu.stanford.spezi.core.design.component.ImageResource -import edu.stanford.spezi.core.design.component.ImageResourceComposable import edu.stanford.spezi.core.design.component.RectangleShimmerEffect +import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.component.VerticalSpacer import edu.stanford.spezi.core.design.component.height import edu.stanford.spezi.core.design.theme.Colors @@ -160,9 +161,8 @@ private fun VideoItem(video: Video, onVideoClick: () -> Unit) { .padding(Spacings.small) .fillMaxWidth() ) { - ImageResourceComposable( - imageResource = ImageResource.Remote(video.thumbnailUrl), - contentDescription = "Video thumbnail", + AsyncImageResourceComposable( + imageResource = AsyncImageResource.Remote(url = video.thumbnailUrl, StringResource("Video thumbnail")), modifier = Modifier .fillMaxWidth() .aspectRatio(ASPECT_16_9) From 307d84cc360e01c32f391f1a5ee643fe3ab8ecfc Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 16:24:28 -0800 Subject: [PATCH 25/51] Adapt contact --- .../edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt | 5 ++++- .../edu/stanford/spezi/modules/contact/ContactComposable.kt | 4 +--- .../edu/stanford/spezi/modules/contact/model/Contact.kt | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt index 4cee98dd8..634a2e1f4 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/contact/ui/ContactScreen.kt @@ -113,7 +113,10 @@ private class ContactUiStateProvider : PreviewParameterProvider Date: Mon, 18 Nov 2024 16:28:30 -0800 Subject: [PATCH 26/51] Add modifier parameters --- .../design/views/views/views/button/ProcessingOverlay.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt index f4242860e..1bae3f4b5 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/views/views/button/ProcessingOverlay.kt @@ -18,11 +18,13 @@ import edu.stanford.spezi.core.design.views.views.model.ViewState @Composable fun ProcessingOverlay( viewState: ViewState, + modifier: Modifier = Modifier, processingContent: @Composable BoxScope.() -> Unit = { CircularProgressIndicator() }, content: @Composable BoxScope.() -> Unit, ) { ProcessingOverlay( isProcessing = viewState == ViewState.Processing, + modifier = modifier, processingContent = processingContent, content = content, ) @@ -31,6 +33,7 @@ fun ProcessingOverlay( @Composable fun ProcessingOverlay( isProcessing: Boolean, + modifier: Modifier = Modifier, processingContent: @Composable BoxScope.() -> Unit = { CircularProgressIndicator() }, content: @Composable BoxScope.() -> Unit, ) { @@ -41,7 +44,7 @@ fun ProcessingOverlay( alpha.floatValue = value } } - Box(contentAlignment = Alignment.Center) { + Box(contentAlignment = Alignment.Center, modifier = modifier) { Box(Modifier.alpha(alpha.floatValue)) { content() } From 088e66f73153754cbaba1d4bd2e5b69fe11ee00b Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 17:27:28 -0800 Subject: [PATCH 27/51] Fix tests --- .../spezi/core/design/personalInfo/UserProfileTest.kt | 3 ++- .../personalInfo/simulators/UserProfileTestSimulator.kt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt index c36d75fcf..ff2cd848c 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt @@ -1,5 +1,6 @@ package edu.stanford.spezi.core.design.personalInfo +import androidx.compose.ui.res.stringResource import androidx.compose.ui.test.junit4.createComposeRule import edu.stanford.spezi.core.design.personalInfo.composables.UserProfileTestComposable import edu.stanford.spezi.core.design.personalInfo.simulators.UserProfileTestSimulator @@ -25,7 +26,7 @@ class UserProfileTest { assertUserInitialsExists(true, "PS") assertUserInitialsExists(true, "LS") waitUntilUserInitialsDisappear("LS") - assertImageExists() + assertImageExists("Person") } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt index 552d409df..7ad6657c4 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/simulators/UserProfileTestSimulator.kt @@ -27,9 +27,9 @@ class UserProfileTestSimulator( } } - fun assertImageExists() { + fun assertImageExists(contentDescription: String) { composeTestRule - .onAllNodesWithContentDescription("") + .onAllNodesWithContentDescription(contentDescription) .assertCountEquals(1) } } From 2207da93013aa7a5c01b9f05e84b8c668feb9211 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 17:30:53 -0800 Subject: [PATCH 28/51] detekt --- .../stanford/spezi/core/design/personalInfo/UserProfileTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt index ff2cd848c..f92376c0f 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/personalInfo/UserProfileTest.kt @@ -1,6 +1,5 @@ package edu.stanford.spezi.core.design.personalInfo -import androidx.compose.ui.res.stringResource import androidx.compose.ui.test.junit4.createComposeRule import edu.stanford.spezi.core.design.personalInfo.composables.UserProfileTestComposable import edu.stanford.spezi.core.design.personalInfo.simulators.UserProfileTestSimulator From c84e74851b6a8ecf4d306054b5abb7ca27c8f90d Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 17:51:40 -0800 Subject: [PATCH 29/51] update --- .../spezi/core/design/component/AsyncImageResource.kt | 8 ++++---- .../stanford/spezi/core/design/component/ImageResource.kt | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResource.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResource.kt index 6d66342f6..1707e4edc 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResource.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/AsyncImageResource.kt @@ -8,28 +8,28 @@ import javax.annotation.concurrent.Immutable @Immutable sealed interface AsyncImageResource { - val identifier: UUID + val identifier: String val contentDescription: StringResource data class Remote( val url: String, override val contentDescription: StringResource, ) : AsyncImageResource { - override val identifier: UUID = UUID() + override val identifier = UUID().toString() } data class Vector( val image: ImageVector, override val contentDescription: StringResource, ) : AsyncImageResource { - override val identifier: UUID = UUID() + override val identifier = UUID().toString() } data class Drawable( @DrawableRes val resId: Int, override val contentDescription: StringResource, ) : AsyncImageResource { - override val identifier: UUID = UUID() + override val identifier = UUID().toString() } companion object { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResource.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResource.kt index a8cc7329b..8099dca74 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResource.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/ImageResource.kt @@ -15,20 +15,20 @@ import javax.annotation.concurrent.Immutable */ @Immutable sealed interface ImageResource { - val identifier: UUID + val identifier: String val contentDescription: StringResource data class Vector( val image: ImageVector, override val contentDescription: StringResource, ) : ImageResource { - override val identifier: UUID = UUID() + override val identifier = UUID().toString() } data class Drawable( @DrawableRes val resId: Int, override val contentDescription: StringResource, ) : ImageResource { - override val identifier: UUID = UUID() + override val identifier = UUID().toString() } } From d184b644a9f45f33dfdda10c60ed51a7a6638578 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 18 Nov 2024 17:54:01 -0800 Subject: [PATCH 30/51] adapt tests --- .../edu/stanford/spezi/modules/contact/ContactFactory.kt | 4 ++-- .../modules/contact/simulator/ContactComposableSimulator.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt index 7c0a17be2..0a1d4d31d 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt @@ -20,7 +20,7 @@ object ContactFactory { givenName = "Leland", familyName = "Stanford" ), - image = ImageResource.Vector(Icons.Default.AccountBox), + image = ImageResource.Vector(Icons.Default.AccountBox, StringResource("Account Box")), title = StringResource("University Founder"), description = StringResource(""" Amasa Leland Stanford (March 9, 1824 – June 21, 1893) was an American industrialist and politician. [...] \ @@ -49,7 +49,7 @@ He and his wife Jane were also the founders of Stanford University, which they n givenName = "Paul", familyName = "Schmiedmayer" ), - image = ImageResource.Vector(Icons.Default.AccountBox), + image = ImageResource.Vector(Icons.Default.AccountBox, StringResource("Account Box")), title = StringResource("A Title"), description = StringResource(""" This is a description of a contact that will be displayed. It might even be longer than what has to be displayed in the contact card. diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt index b3c48d390..26da4f5ee 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt @@ -48,7 +48,7 @@ class ContactComposableSimulator( imageResource?.let { image(imageResource) .assertExists() - .assertContentDescriptionContains("Profile Picture") + .assertContentDescriptionContains("Account Box") .assertImageIdentifier(it.identifier) } } From 58dc2695ccef3f8c4b6bb7acd2df0a01641b4760 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Tue, 19 Nov 2024 10:27:34 -0800 Subject: [PATCH 31/51] small update --- .../core/design/views/validation/views/VerifiableTextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index 7f62913b2..2de76082e 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -37,13 +37,13 @@ fun VerifiableTextField( footer: @Composable () -> Unit = {}, ) { VerifiableTextField( + label = label, value = state.value, onValueChanged = { state.value = it }, modifier = modifier, type = type, disableAutocorrection = disableAutocorrection, footer = footer, - label = { Text(label.text()) }, ) } From 858c75fb4f5e3ebb20465cb32ac5493827da463e Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Tue, 19 Nov 2024 12:53:23 -0800 Subject: [PATCH 32/51] Make sure to only trigger validation after the first input --- .../validation/FocusValidationRulesTest.kt | 6 +++ .../composables/FocusValidationRules.kt | 22 ++++++++-- .../FocusValidationRulesSimulator.kt | 15 +++++++ .../views/validation/ValidationEngine.kt | 12 +++-- .../views/validation/ValidationModifier.kt | 9 +++- .../validation/views/VerifiableTextField.kt | 44 +++++++++---------- 6 files changed, 77 insertions(+), 31 deletions(-) diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/FocusValidationRulesTest.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/FocusValidationRulesTest.kt index 3466f8c26..2f8536da4 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/FocusValidationRulesTest.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/FocusValidationRulesTest.kt @@ -30,6 +30,12 @@ class FocusValidationRulesTest { assertLastState(false) assertPasswordMessageExists(true) assertEmptyMessageExists(true) + enterEmail("leland@stanford.edu") + assertEmptyMessageExists(false) + assertPasswordMessageExists(true) + enterPassword("password") + assertEmptyMessageExists(false) + assertPasswordMessageExists(false) } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt index e62765447..91537dfbb 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/composables/FocusValidationRules.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.validation.Validate import edu.stanford.spezi.core.design.views.validation.ValidationRule @@ -16,11 +17,16 @@ import edu.stanford.spezi.core.design.views.validation.nonEmpty import edu.stanford.spezi.core.design.views.validation.state.ReceiveValidation import edu.stanford.spezi.core.design.views.validation.state.ValidationContext import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField +import edu.stanford.spezi.core.utils.extensions.testIdentifier enum class Field { INPUT, NON_EMPTY_INPUT } +enum class FocusValidationRulesTestIdentifier { + EMAIL_TEXTFIELD, PASSWORD_TEXTFIELD +} + @Composable fun FocusValidationRules() { val input = remember { mutableStateOf("") } @@ -50,12 +56,20 @@ fun FocusValidationRules() { Switch(switchFocus.value, onCheckedChange = { switchFocus.value = it }) } - Validate(input.value, rules = listOf(ValidationRule.minimalPassword)) { - VerifiableTextField(StringResource(Field.INPUT.name), input) + Validate(nonEmptyInput.value, rules = listOf(ValidationRule.nonEmpty)) { + VerifiableTextField( + StringResource(Field.NON_EMPTY_INPUT.name), + nonEmptyInput, + Modifier.testIdentifier(FocusValidationRulesTestIdentifier.EMAIL_TEXTFIELD) + ) } - Validate(nonEmptyInput.value, rules = listOf(ValidationRule.nonEmpty)) { - VerifiableTextField(StringResource(Field.NON_EMPTY_INPUT.name), nonEmptyInput) + Validate(input.value, rules = listOf(ValidationRule.minimalPassword)) { + VerifiableTextField( + StringResource(Field.INPUT.name), + input, + Modifier.testIdentifier(FocusValidationRulesTestIdentifier.PASSWORD_TEXTFIELD) + ) } } } diff --git a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/simulators/FocusValidationRulesSimulator.kt b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/simulators/FocusValidationRulesSimulator.kt index 62d828e16..0e0a4affc 100644 --- a/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/simulators/FocusValidationRulesSimulator.kt +++ b/core/design/src/androidTest/kotlin/edu/stanford/spezi/core/design/validation/simulators/FocusValidationRulesSimulator.kt @@ -4,6 +4,9 @@ import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import edu.stanford.spezi.core.design.validation.composables.FocusValidationRulesTestIdentifier +import edu.stanford.spezi.core.testing.onNodeWithIdentifier class FocusValidationRulesSimulator( private val composeTestRule: ComposeTestRule, @@ -45,4 +48,16 @@ class FocusValidationRulesSimulator( .onNodeWithText("Last state: ${if (valid) "valid" else "invalid"}") .assertExists() } + + fun enterEmail(text: String) { + composeTestRule + .onNodeWithIdentifier(FocusValidationRulesTestIdentifier.EMAIL_TEXTFIELD) + .performTextInput(text) + } + + fun enterPassword(text: String) { + composeTestRule + .onNodeWithIdentifier(FocusValidationRulesTestIdentifier.PASSWORD_TEXTFIELD) + .performTextInput(text) + } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt index 42f50e87d..779af4367 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt @@ -3,6 +3,7 @@ package edu.stanford.spezi.core.design.views.validation import androidx.compose.runtime.mutableStateOf import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult +import edu.stanford.spezi.core.logging.speziLogger import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job @@ -41,6 +42,8 @@ internal class ValidationEngineImpl( SUBMIT, MANUAL } + private val logger by speziLogger() + private var validationResultsState = mutableStateOf(emptyList()) override val validationResults get() = validationResultsState.value @@ -68,14 +71,15 @@ internal class ValidationEngineImpl( private var debounceJob: Job? = null - @Suppress("detekt:LoopWithTooManyJumpStatements") private fun computeFailedValidations(input: String): List { val results = mutableListOf() + @Suppress("detekt:LoopWithTooManyJumpStatements") for (rule in rules) { - val result = rule.validate(input) ?: break - results.add(result) - // TODO: Logging + rule.validate(input)?.let { result -> + results.add(result) + logger.w { "Validation for input $input failed with reason: ${result.message}" } + } ?: continue if (rule.effect == CascadingValidationEffect.INTERCEPT) break } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt index 5ee5122c3..c00f6b9f2 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationModifier.kt @@ -4,8 +4,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION @@ -53,8 +55,13 @@ fun Validate( ) } + var isFirstInput by remember { mutableStateOf(true) } LaunchedEffect(input) { - engine.submit(input, debounce = true) + if (isFirstInput) { + isFirstInput = false + } else { + engine.submit(input, debounce = true) + } } LaunchedEffect(validationDebounce) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index 2de76082e..db74d039d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.material3.TextField @@ -16,6 +17,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.validation.Validate @@ -101,28 +103,26 @@ fun VerifiableTextField( val validationEngine = LocalValidationEngine.current val isSecure = remember(type) { type == TextFieldType.SECURE } - Column(modifier) { - // TODO: Check equality with iOS - TextField( - value = value, - onValueChange = onValueChanged, - label = label, - keyboardOptions = KeyboardOptions( - keyboardType = if (isSecure) KeyboardType.Password else KeyboardType.Text, - autoCorrect = !disableAutocorrection - ), - visualTransformation = if (isSecure) PasswordVisualTransformation() else VisualTransformation.None, - modifier = Modifier.fillMaxWidth(), - ) - - Row { - ValidationResultsComposable(validationEngine?.displayedValidationResults ?: emptyList()) - - Spacer(Modifier.fillMaxWidth()) - - footer() - } - } + // TODO: Check equality with iOS + TextField( + value = value, + onValueChange = onValueChanged, + label = label, + keyboardOptions = KeyboardOptions( + keyboardType = if (isSecure) KeyboardType.Password else KeyboardType.Text, + autoCorrect = !disableAutocorrection + ), + supportingText = { + Row(Modifier.padding(vertical = Spacings.small)) { + ValidationResultsComposable(validationEngine?.displayedValidationResults ?: emptyList()) + Spacer(Modifier.fillMaxWidth()) + footer() + } + }, + isError = validationEngine?.isDisplayingValidationErrors ?: true, + visualTransformation = if (isSecure) PasswordVisualTransformation() else VisualTransformation.None, + modifier = modifier.fillMaxWidth(), + ) } @ThemePreviews From f3af7e3cdbea34839a1cfd80c830b72b0cb87725 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Tue, 19 Nov 2024 12:54:54 -0800 Subject: [PATCH 33/51] detekt --- .../core/design/views/validation/views/VerifiableTextField.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index db74d039d..5bc7bd584 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -1,6 +1,5 @@ package edu.stanford.spezi.core.design.views.validation.views -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth From ace70d4b8ba612ace0fc06896b45a17ab1551b17 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Tue, 19 Nov 2024 13:06:49 -0800 Subject: [PATCH 34/51] Add onSubmit --- .../design/views/validation/views/VerifiableTextField.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index 5bc7bd584..859b3b2f6 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.material3.TextField @@ -107,6 +108,11 @@ fun VerifiableTextField( value = value, onValueChange = onValueChanged, label = label, + keyboardActions = KeyboardActions( + onDone = { + validationEngine?.submit(value) + }, + ), keyboardOptions = KeyboardOptions( keyboardType = if (isSecure) KeyboardType.Password else KeyboardType.Text, autoCorrect = !disableAutocorrection From d61539fa13e7b2b82104adacc43fbf81ab12c245 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Fri, 22 Nov 2024 15:27:17 -0800 Subject: [PATCH 35/51] detekt --- .../core/design/component/markdown/MarkdownUiState.kt | 4 ++-- .../spezi/module/onboarding/consent/ConsentDocument.kt | 5 ++--- .../module/onboarding/consent/ConsentDocumentExport.kt | 2 +- ...iguration.kt => ConsentDocumentExportConfiguration.kt} | 0 .../spezi/module/onboarding/consent/ConsentUiState.kt | 2 +- .../onboarding/consent/OnboardingConsentComposable.kt | 2 +- .../module/onboarding/onboarding/OnboardingActions.kt | 4 ++-- .../module/onboarding/onboarding/OnboardingComposable.kt | 3 +-- .../onboarding/onboarding/OnboardingComposableBuilder.kt | 8 ++++---- .../onboarding/flow/IllegalOnboardingStepComposable.kt | 2 +- .../spezi/module/onboarding/views/SuspendButton.kt | 6 +++--- .../stanford/spezi/module/onboarding/views/ViewState.kt | 2 +- .../spezi/module/onboarding/views/ViewStateAlert.kt | 2 +- 13 files changed, 20 insertions(+), 22 deletions(-) rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/{ExportConfiguration.kt => ConsentDocumentExportConfiguration.kt} (100%) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt index 87ed6e178..d18650d1c 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt @@ -1,5 +1,5 @@ package edu.stanford.spezi.core.design.component.markdown data class MarkdownUiState( - val elements: List? = null -) \ No newline at end of file + val elements: List? = null, +) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt index 0071ca6b2..68b67c37f 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt @@ -50,7 +50,7 @@ data class ConsentDocument( internal fun Composable( modifier: Modifier = Modifier, uiState: ConsentUiState, - onAction: (ConsentAction) -> Unit + onAction: (ConsentAction) -> Unit, ) { val givenName = uiState.name.givenName ?: "" val familyName = uiState.name.familyName ?: "" @@ -86,7 +86,6 @@ data class ConsentDocument( } ) - if (givenName.isNotBlank() && familyName.isNotBlank()) { Spacer(modifier = Modifier.height(Spacings.medium)) Text("Signature:") @@ -128,7 +127,7 @@ private fun ConsentDocumentComposablePreview( ConsentDocument( markdown = { "".toByteArray(StandardCharsets.UTF_8) }, viewState = remember { mutableStateOf(ConsentViewState.Base(ViewState.Idle)) }, - ).Composable( + ).Composable( uiState = ConsentUiState( name = data.name, paths = data.paths diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt index 022d75d50..63a10e3ee 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt @@ -4,7 +4,7 @@ import android.graphics.pdf.PdfDocument class ConsentDocumentExport( private val documentIdentifier: String = Defaults.DOCUMENT_IDENTIFIER, - private val document: suspend () -> PdfDocument + private val document: suspend () -> PdfDocument, ) { private object Defaults { const val DOCUMENT_IDENTIFIER = "ConsentDocument" diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExportConfiguration.kt similarity index 100% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExportConfiguration.kt diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt index 3927f2f27..a5130e726 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt @@ -29,6 +29,6 @@ sealed interface ConsentAction { data object Undo : ConsentAction data class Consent( val documentIdentifier: String, - val exportConfiguration: ConsentDocumentExportConfiguration + val exportConfiguration: ConsentDocumentExportConfiguration, ) : ConsentAction } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt index 428572aa7..194f85883 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt @@ -62,7 +62,7 @@ internal fun OnboardingConsentComposableContent( identifier: String, exportConfiguration: ConsentDocumentExportConfiguration, uiState: ConsentUiState, - onAction: (ConsentAction) -> Unit + onAction: (ConsentAction) -> Unit, ) { val actionScope = rememberCoroutineScope() OnboardingComposable( diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt index 9279176eb..d33a926e0 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt @@ -20,7 +20,7 @@ fun OnboardingActions( primaryText: StringResource, primaryAction: suspend () -> Unit, secondaryText: StringResource? = null, - secondaryAction: (suspend () -> Unit)? = null + secondaryAction: (suspend () -> Unit)? = null, ) { val primaryActionState = remember { mutableStateOf(ViewState.Idle) } val secondaryActionState = remember { mutableStateOf(ViewState.Idle) } @@ -45,4 +45,4 @@ fun OnboardingActions( } } } -} \ No newline at end of file +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt index f40a698d8..476797fa4 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp - @Composable fun OnboardingComposable( modifier: Modifier = Modifier, @@ -40,4 +39,4 @@ fun OnboardingComposable( } } } -} \ No newline at end of file +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt index c32b6e9de..4df3dccd0 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt @@ -5,11 +5,11 @@ import androidx.compose.runtime.Composable data class OnboardingStep( val identifier: String, - val composable: @Composable () -> Unit + val composable: @Composable () -> Unit, ) data class OnboardingComposableBuilder( - var list: MutableList + var list: MutableList, ) { fun step(id: String, composable: @Composable () -> Unit) { list.add(OnboardingStep(id, composable)) @@ -17,7 +17,7 @@ data class OnboardingComposableBuilder( } fun buildOnboardingSteps( - build: OnboardingComposableBuilder.() -> Unit + build: OnboardingComposableBuilder.() -> Unit, ): List { val builder = OnboardingComposableBuilder(mutableListOf()) build(builder) @@ -38,4 +38,4 @@ fun test() { } } } -} \ No newline at end of file +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt index 14f99b9f8..472e9902b 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt @@ -7,4 +7,4 @@ import edu.stanford.spezi.core.design.component.StringResource @Composable internal fun IllegalOnboardingStepComposable() { Text(StringResource("Illegal onboarding step").text()) -} \ No newline at end of file +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt index 52bce60cb..4a488c3fa 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt @@ -13,14 +13,14 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch private enum class SuspendButtonState { - IDLE, DISABLED, DISABLED_AND_PROCESSING; + IDLE, DISABLED, DISABLED_AND_PROCESSING } @Composable fun SuspendButton( state: MutableState, action: suspend () -> Unit, - label: @Composable () -> Unit + label: @Composable () -> Unit, ) { val buttonState = remember { mutableStateOf(SuspendButtonState.IDLE) } val coroutineScope = rememberCoroutineScope() @@ -58,4 +58,4 @@ fun SuspendButton( ) { label() } -} \ No newline at end of file +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt index 787b05b31..4000331e0 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt @@ -14,4 +14,4 @@ sealed interface ViewState { val errorDescription: String @Composable @ReadOnlyComposable get() = if (this is Error) throwable?.localizedMessage ?: "" else "" -} \ No newline at end of file +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt index df758c582..332d9e016 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt @@ -30,4 +30,4 @@ fun ViewStateAlert(state: MutableState) { } ) } -} \ No newline at end of file +} From 7a94909902b5c6bf0926c6452b487f6d36eedfd4 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Fri, 22 Nov 2024 15:40:31 -0800 Subject: [PATCH 36/51] Merge completed --- .../edu/stanford/spezi/core/utils/Standard.kt | 3 + .../onboarding/di/TestOnboardingModule.kt | 4 -- .../onboarding/consent/ConsentConstraint.kt | 2 +- .../onboarding/consent/ConsentDataSource.kt | 9 ++- .../onboarding/consent/ConsentDocument.kt | 3 +- .../onboarding/consent/ConsentPdfService.kt | 1 + .../onboarding/consent/ConsentUiState.kt | 3 +- .../onboarding/consent/ConsentViewState.kt | 2 +- .../consent/OnboardingConsentComposable.kt | 3 +- .../onboarding/OnboardingActions.kt | 19 +++--- .../spezi/module/onboarding/views/Standard.kt | 3 - .../module/onboarding/views/SuspendButton.kt | 61 ------------------- .../module/onboarding/views/ViewState.kt | 17 ------ .../module/onboarding/views/ViewStateAlert.kt | 33 ---------- 14 files changed, 27 insertions(+), 136 deletions(-) create mode 100644 core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Standard.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt diff --git a/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Standard.kt b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Standard.kt new file mode 100644 index 000000000..54e3035a8 --- /dev/null +++ b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Standard.kt @@ -0,0 +1,3 @@ +package edu.stanford.spezi.core.utils + +interface Standard diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt index 1c7bf87de..46495a9ec 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt @@ -42,8 +42,4 @@ class TestOnboardingModule { @Provides @Singleton fun provideSequentialOnboardingRepository(): SequentialOnboardingRepository = mockk() - - @Provides - @Singleton - fun provideOnConsentRepository(): ConsentManager = mockk() } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt index c388f1347..b5f4c9169 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt @@ -1,6 +1,6 @@ package edu.stanford.spezi.module.onboarding.consent -import edu.stanford.spezi.module.onboarding.views.Standard +import edu.stanford.spezi.core.utils.Standard interface ConsentConstraint : Standard { suspend fun store(consent: ConsentDocumentExport) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt index f1837b081..68f63b378 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt @@ -1,11 +1,14 @@ package edu.stanford.spezi.module.onboarding.consent import android.graphics.pdf.PdfDocument -import edu.stanford.spezi.module.onboarding.views.Standard +import edu.stanford.spezi.core.utils.Standard import javax.inject.Inject -class ConsentDataSource { - @Inject lateinit var standard: Standard +class MyStandard : Standard + +class ConsentDataSource @Inject constructor() { + // TODO: Inject standard here + var standard: Standard = MyStandard() init { if (standard !is ConsentConstraint) { diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt index 68b67c37f..d1ebafb20 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt @@ -26,7 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.Spacings -import edu.stanford.spezi.module.onboarding.views.ViewState +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.views.model.ViewState import java.nio.charset.StandardCharsets data class ConsentDocument( diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt index 5d6107893..727b2bc1a 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asAndroidPath import edu.stanford.spezi.core.coroutines.di.Dispatching import edu.stanford.spezi.core.design.component.markdown.MarkdownElement +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import java.time.LocalDate diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt index a5130e726..46d46bbe2 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt @@ -2,7 +2,8 @@ package edu.stanford.spezi.module.onboarding.consent import androidx.compose.ui.graphics.Path import edu.stanford.spezi.core.design.component.markdown.MarkdownElement -import edu.stanford.spezi.module.onboarding.views.ViewState +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.views.model.ViewState internal data class ConsentUiState( val name: PersonNameComponents = PersonNameComponents(), diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt index eda047335..d7520318c 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt @@ -1,7 +1,7 @@ package edu.stanford.spezi.module.onboarding.consent import android.graphics.pdf.PdfDocument -import edu.stanford.spezi.module.onboarding.views.ViewState +import edu.stanford.spezi.core.design.views.views.model.ViewState sealed interface ConsentViewState { data class Base(val viewState: ViewState) : ConsentViewState diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt index 194f85883..7d9bf678e 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt @@ -26,10 +26,11 @@ import edu.stanford.spezi.core.design.component.markdown.MarkdownComposable import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.views.model.ViewState import edu.stanford.spezi.core.utils.extensions.testIdentifier import edu.stanford.spezi.module.onboarding.onboarding.OnboardingComposable import edu.stanford.spezi.module.onboarding.onboarding.OnboardingTitle -import edu.stanford.spezi.module.onboarding.views.ViewState import kotlinx.coroutines.launch @Composable diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt index d33a926e0..c97cab18d 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt @@ -10,16 +10,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.module.onboarding.views.SuspendButton -import edu.stanford.spezi.module.onboarding.views.ViewState -import edu.stanford.spezi.module.onboarding.views.ViewStateAlert +import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.core.design.views.views.views.button.SuspendButton +import edu.stanford.spezi.core.design.views.views.viewstate.ViewStateAlert @Composable fun OnboardingActions( - primaryText: StringResource, + primaryText: String, primaryAction: suspend () -> Unit, - secondaryText: StringResource? = null, + secondaryText: String? = null, secondaryAction: (suspend () -> Unit)? = null, ) { val primaryActionState = remember { mutableStateOf(ViewState.Idle) } @@ -29,9 +28,9 @@ fun OnboardingActions( ViewStateAlert(secondaryActionState) Column(Modifier.padding(top = 10.dp)) { - SuspendButton(primaryActionState, primaryAction) { + SuspendButton(state = primaryActionState, action = primaryAction) { Text( - primaryText.text(), + primaryText, modifier = Modifier .fillMaxWidth() .heightIn(min = 38.dp) @@ -39,8 +38,8 @@ fun OnboardingActions( } secondaryText?.let { secondaryText -> secondaryAction?.let { secondaryAction -> - SuspendButton(secondaryActionState, secondaryAction) { - Text(secondaryText.text()) + SuspendButton(state = secondaryActionState, action = secondaryAction) { + Text(secondaryText) } } } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt deleted file mode 100644 index bb196a0a2..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt +++ /dev/null @@ -1,3 +0,0 @@ -package edu.stanford.spezi.module.onboarding.views - -interface Standard diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt deleted file mode 100644 index 4a488c3fa..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt +++ /dev/null @@ -1,61 +0,0 @@ -package edu.stanford.spezi.module.onboarding.views - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import edu.stanford.spezi.core.design.component.Button -import edu.stanford.spezi.core.utils.UUID -import kotlinx.coroutines.cancel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch - -private enum class SuspendButtonState { - IDLE, DISABLED, DISABLED_AND_PROCESSING -} - -@Composable -fun SuspendButton( - state: MutableState, - action: suspend () -> Unit, - label: @Composable () -> Unit, -) { - val buttonState = remember { mutableStateOf(SuspendButtonState.IDLE) } - val coroutineScope = rememberCoroutineScope() - val debounceScope = rememberCoroutineScope() - - DisposableEffect(remember { UUID() }) { - onDispose { - coroutineScope.cancel() - } - } - - Button( - onClick = { - if (state.value == ViewState.Processing) return@Button - buttonState.value = SuspendButtonState.DISABLED - - // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? - state.value = ViewState.Processing - - coroutineScope.launch { - runCatching { - action() - if (state.value != ViewState.Idle) { - // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? - state.value = ViewState.Idle - } - }.onFailure { - state.value = ViewState.Error(it) - } - - buttonState.value = SuspendButtonState.IDLE - } - }, - enabled = !coroutineScope.isActive - ) { - label() - } -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt deleted file mode 100644 index 4000331e0..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package edu.stanford.spezi.module.onboarding.views - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import edu.stanford.spezi.core.design.component.StringResource - -sealed interface ViewState { - data object Idle : ViewState - data object Processing : ViewState - data class Error(val throwable: Throwable?) : ViewState - - val errorTitle: String - @Composable @ReadOnlyComposable get() = StringResource("Error").text() - - val errorDescription: String - @Composable @ReadOnlyComposable get() = if (this is Error) throwable?.localizedMessage ?: "" else "" -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt deleted file mode 100644 index 332d9e016..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt +++ /dev/null @@ -1,33 +0,0 @@ -package edu.stanford.spezi.module.onboarding.views - -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState - -@Composable -fun ViewStateAlert(state: MutableState) { - if (state.value is ViewState.Error) { - AlertDialog( - title = { - Text(text = state.value.errorTitle) - }, - text = { - Text(text = state.value.errorDescription) - }, - onDismissRequest = { - state.value = ViewState.Idle - }, - confirmButton = { - TextButton( - onClick = { - state.value = ViewState.Idle - } - ) { - Text("Okay") - } - } - ) - } -} From f4bcd7a680d78aa1268a412d6f275053698f109c Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Fri, 22 Nov 2024 18:53:16 -0800 Subject: [PATCH 37/51] Improve onboarding --- .../onboarding/consent/ConsentDataSource.kt | 4 +- .../consent/OnboardingConsentComposable.kt | 8 +- .../OnboardingComposable.kt | 14 +- .../module/onboarding/core/OnboardingTitle.kt | 46 ++++++ .../onboarding/onboarding/OnboardingTitle.kt | 37 ----- .../flow/IllegalOnboardingStepComposable.kt | 10 -- .../OnboardingActions.kt | 35 +++- .../OnboardingComposableBuilder.kt | 2 +- .../spezi/OnboardingComposableInformation.kt | 67 ++++++++ .../onboarding/spezi/OnboardingInformation.kt | 116 +++++++++++++ .../spezi/SequentialOnboardingComposable.kt | 156 ++++++++++++++++++ .../spezi/flow/IllegalOnboardingStep.kt | 11 ++ 12 files changed, 442 insertions(+), 64 deletions(-) rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{onboarding => core}/OnboardingComposable.kt (71%) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/core/OnboardingTitle.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{onboarding => spezi}/OnboardingActions.kt (62%) rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{onboarding => spezi}/OnboardingComposableBuilder.kt (93%) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingComposableInformation.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingInformation.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingComposable.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/IllegalOnboardingStep.kt diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt index 68f63b378..28d38df0e 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt @@ -12,7 +12,7 @@ class ConsentDataSource @Inject constructor() { init { if (standard !is ConsentConstraint) { - TODO("on iOS: fatalError") + error("Standard does not conform to ConsentConstraint!") } } @@ -20,6 +20,6 @@ class ConsentDataSource @Inject constructor() { (standard as? ConsentConstraint)?.let { consentConstraint -> val export = ConsentDocumentExport(identifier, document) consentConstraint.store(export) - } ?: TODO("on iOS: fatalError") + } ?: error("Standard does not conform to ConsentConstraint!") } } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt index 7d9bf678e..9b32122c2 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt @@ -29,15 +29,15 @@ import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.views.model.ViewState import edu.stanford.spezi.core.utils.extensions.testIdentifier -import edu.stanford.spezi.module.onboarding.onboarding.OnboardingComposable -import edu.stanford.spezi.module.onboarding.onboarding.OnboardingTitle +import edu.stanford.spezi.module.onboarding.core.OnboardingComposable +import edu.stanford.spezi.module.onboarding.core.OnboardingTitle import kotlinx.coroutines.launch @Composable fun OnboardingConsentComposable( markdown: suspend () -> ByteArray, action: suspend () -> Unit, - title: StringResource? = remember { StringResource("Consent") }, + title: String? = StringResource("Consent").text(), identifier: String = remember { "ConsentDocument" }, exportConfiguration: ConsentDocumentExportConfiguration = remember { ConsentDocumentExportConfiguration() }, ) { @@ -59,7 +59,7 @@ fun OnboardingConsentComposable( internal fun OnboardingConsentComposableContent( markdown: suspend () -> ByteArray, action: suspend () -> Unit, - title: StringResource?, + title: String?, identifier: String, exportConfiguration: ConsentDocumentExportConfiguration, uiState: ConsentUiState, diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/core/OnboardingComposable.kt similarity index 71% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/core/OnboardingComposable.kt index 476797fa4..5592c6d2c 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/core/OnboardingComposable.kt @@ -1,14 +1,18 @@ -package edu.stanford.spezi.module.onboarding.onboarding +package edu.stanford.spezi.module.onboarding.core import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntSize @@ -23,15 +27,15 @@ fun OnboardingComposable( ) { val size = remember { mutableStateOf(IntSize.Zero) } Box(modifier.onSizeChanged { size.value = it }) { - LazyColumn { + LazyColumn(Modifier.padding(24.dp)) { item { - Column(Modifier.heightIn(min = size.value.height.dp)) { - Column { + Column(Modifier.heightIn(min = size.value.height.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { title() content() } action?.let { action -> - Spacer(Modifier) + Spacer(Modifier.fillMaxHeight()) action() } Spacer(Modifier.height(10.dp)) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/core/OnboardingTitle.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/core/OnboardingTitle.kt new file mode 100644 index 000000000..a27650449 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/core/OnboardingTitle.kt @@ -0,0 +1,46 @@ +package edu.stanford.spezi.module.onboarding.core + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import edu.stanford.spezi.core.design.theme.Spacings +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.TextStyles +import edu.stanford.spezi.core.design.theme.ThemePreviews + +@Composable +fun OnboardingTitle( + title: String, + subtitle: String? = null, +) { + Column(Modifier.padding(vertical = Spacings.medium), horizontalAlignment = Alignment.CenterHorizontally) { + Text( + title, + modifier = Modifier.padding(bottom = Spacings.medium), + style = TextStyles.headlineMedium.copy(fontWeight = FontWeight.Bold), + textAlign = TextAlign.Center + ) + + subtitle?.let { subtitle -> + Text( + subtitle, + modifier = Modifier.padding(bottom = Spacings.medium), + style = TextStyles.bodyMedium, + textAlign = TextAlign.Center + ) + } + } +} + +@ThemePreviews +@Composable +private fun OnboardingTitlePreview() { + SpeziTheme(isPreview = true) { + OnboardingTitle("Title", "Subtitle") + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt deleted file mode 100644 index 4a775e512..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt +++ /dev/null @@ -1,37 +0,0 @@ -package edu.stanford.spezi.module.onboarding.onboarding - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import edu.stanford.spezi.core.design.component.StringResource - -@Composable -fun OnboardingTitle(title: StringResource, subtitle: StringResource? = null) { - OnboardingTitle(title.text(), subtitle?.text()) -} - -@Composable -fun OnboardingTitle(title: String, subtitle: String? = null) { - Column(Modifier.padding(vertical = 8.dp)) { - Text( - title, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - textAlign = TextAlign.Center - ) - - subtitle?.let { subtitle -> - Text( - subtitle, - modifier = Modifier.padding(bottom = 8.dp), - textAlign = TextAlign.Center - ) - } - } -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt deleted file mode 100644 index 472e9902b..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt +++ /dev/null @@ -1,10 +0,0 @@ -package edu.stanford.spezi.module.onboarding.onboarding.flow - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import edu.stanford.spezi.core.design.component.StringResource - -@Composable -internal fun IllegalOnboardingStepComposable() { - Text(StringResource("Illegal onboarding step").text()) -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingActions.kt similarity index 62% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingActions.kt index c97cab18d..466c83488 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingActions.kt @@ -1,5 +1,6 @@ -package edu.stanford.spezi.module.onboarding.onboarding +package edu.stanford.spezi.module.onboarding.spezi +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn @@ -8,8 +9,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.views.model.ViewState import edu.stanford.spezi.core.design.views.views.views.button.SuspendButton import edu.stanford.spezi.core.design.views.views.viewstate.ViewStateAlert @@ -27,17 +32,24 @@ fun OnboardingActions( ViewStateAlert(primaryActionState) ViewStateAlert(secondaryActionState) - Column(Modifier.padding(top = 10.dp)) { + Column(Modifier.padding(top = 10.dp), horizontalAlignment = Alignment.CenterHorizontally) { SuspendButton(state = primaryActionState, action = primaryAction) { - Text( - primaryText, + Box( + contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() .heightIn(min = 38.dp) - ) + ) { + Text( + primaryText, + textAlign = TextAlign.Center, + ) + } } + secondaryText?.let { secondaryText -> secondaryAction?.let { secondaryAction -> + // TODO: Make SuspendTextButton SuspendButton(state = secondaryActionState, action = secondaryAction) { Text(secondaryText) } @@ -45,3 +57,16 @@ fun OnboardingActions( } } } + +@ThemePreviews +@Composable +private fun OnboardingActionsPreview() { + SpeziTheme(isPreview = true) { + OnboardingActions( + "Primary", + { println("Primary Action") }, + "Secondary", + { println("Secondary Action") } + ) + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingComposableBuilder.kt similarity index 93% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingComposableBuilder.kt index 4df3dccd0..9fc121b97 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingComposableBuilder.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.module.onboarding.onboarding +package edu.stanford.spezi.module.onboarding.spezi import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingComposableInformation.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingComposableInformation.kt new file mode 100644 index 000000000..ae15ba75b --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingComposableInformation.kt @@ -0,0 +1,67 @@ +package edu.stanford.spezi.module.onboarding.spezi + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.Call +import androidx.compose.material.icons.filled.Email +import androidx.compose.runtime.Composable +import edu.stanford.spezi.core.design.component.ImageResource +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.component.StringResource.Companion.invoke +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.module.onboarding.core.OnboardingComposable +import edu.stanford.spezi.module.onboarding.core.OnboardingTitle + +@Composable +fun OnboardingComposable( + title: String, + subtitle: String? = null, + areas: List, + actionText: String, + action: suspend () -> Unit, +) { + OnboardingComposable( + title = { + OnboardingTitle(title, subtitle) + }, + content = { + OnboardingInformation(areas) + }, + action = { + OnboardingActions(actionText, action) + } + ) +} + +@ThemePreviews +@Composable +private fun OnboardingComposablePreview() { + val areas = listOf( + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Email, StringResource("Email")), + title = "Email", + description = "This is an email. And we can write a lot about E-Mails in a section like this. A very long text!" + ), + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Build, StringResource("Wrench")), + title = "Wrench", + description = "This is a wrench!" + ), + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Call, StringResource("Phone")), + title = "Phone", + description = "This is a phone." + ) + ) + + SpeziTheme(isPreview = true) { + OnboardingComposable( + "Title", + "Subtitle", + areas, + actionText = "Action", + action = {} + ) + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingInformation.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingInformation.kt new file mode 100644 index 000000000..51478bee2 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/OnboardingInformation.kt @@ -0,0 +1,116 @@ +package edu.stanford.spezi.module.onboarding.spezi + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.Call +import androidx.compose.material.icons.filled.Email +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.component.ImageResource +import edu.stanford.spezi.core.design.component.ImageResourceComposable +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.component.StringResource.Companion.invoke +import edu.stanford.spezi.core.design.theme.Colors +import edu.stanford.spezi.core.design.theme.Spacings +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.TextStyles +import edu.stanford.spezi.core.design.theme.ThemePreviews + +data class OnboardingInformationContent( + val icon: @Composable () -> Unit, + val title: String, + val description: String, +) { + constructor( + icon: ImageResource, + title: String, + description: String, + ) : this( + icon = { ImageResourceComposable(icon) }, + title = title, + description = description, + ) +} + +@Composable +fun OnboardingInformation(areas: List) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(30.dp) + ) { + for (area in areas) { + OnboardingInformationArea(area) + } + } +} + +@Composable +private fun OnboardingInformationArea(content: OnboardingInformationContent) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Box( + modifier = Modifier + .padding(Spacings.small) + .padding(end = 10.dp) + .width(40.dp), + ) { + content.icon() + } + + Column { + Text( + content.title, + style = TextStyles.bodyMedium.copy( + fontWeight = FontWeight.Bold, + ), + ) + + Text( + content.description, + style = TextStyles.bodyMedium.copy( + color = Colors.secondary, + ), + ) + } + } +} + +@ThemePreviews +@Composable +private fun OnboardingInformationPreview() { + val areas = listOf( + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Email, StringResource("Email")), + title = "Email", + description = "This is an email. And we can write a lot about E-Mails in a section like this. A very long text!" + ), + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Build, StringResource("Wrench")), + title = "Wrench", + description = "This is a wrench!" + ), + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Call, StringResource("Phone")), + title = "Phone", + description = "This is a phone." + ) + ) + + SpeziTheme(isPreview = true) { + OnboardingInformation(areas) + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingComposable.kt new file mode 100644 index 000000000..87c01bd8f --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingComposable.kt @@ -0,0 +1,156 @@ +package edu.stanford.spezi.module.onboarding.spezi + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.Colors +import edu.stanford.spezi.core.design.theme.Spacings +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.TextStyles +import edu.stanford.spezi.core.design.theme.ThemePreviews +import edu.stanford.spezi.module.onboarding.core.OnboardingComposable +import edu.stanford.spezi.module.onboarding.core.OnboardingTitle + +data class SequentialOnboardingContent( + val title: String?, + val description: String, +) + +@Composable +fun SequentialOnboardingComposable( + title: String, + subtitle: String? = null, + content: List, + actionText: String, + action: suspend () -> Unit, +) { + SequentialOnboardingComposable( + title = { + OnboardingTitle( + title, + subtitle, + ) + }, + content = content, + actionText = actionText, + action = action, + ) +} + +@Composable +fun SequentialOnboardingComposable( + title: @Composable () -> Unit, + content: List, + actionText: String, + action: suspend () -> Unit, +) { + val currentContentIndex = remember { mutableIntStateOf(0) } + OnboardingComposable( + title = { + title() + }, + content = { + for (index in content.indices) { + if (index <= currentContentIndex.intValue) { + SequentialOnboardingStep(index, content[index]) + } + } + }, + action = { + val isDone = currentContentIndex.intValue >= content.size + OnboardingActions( + primaryText = if (isDone) actionText else StringResource("Next").text(), + primaryAction = { + if (!isDone) { + currentContentIndex.intValue++ + } else { + action() + } + } + ) + } + ) +} + +@Composable +private fun SequentialOnboardingStep( + index: Int, + content: SequentialOnboardingContent, +) { + Row(modifier = Modifier + .padding(bottom = Spacings.small) + .background(Colors.primary.copy(alpha = 0.1f), RoundedCornerShape(16.dp)) + .padding(horizontal = 12.dp) + .padding(top = 4.dp, bottom = 12.dp) + .fillMaxWidth(), + ) { + Row { + Text( + "${index + 1}", + style = TextStyles.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = Colors.background, + modifier = Modifier + .padding(4.dp) + // TODO: Figure out how to achieve an actual circle here without using a preset size + .background(Colors.secondary, CircleShape) + .padding(8.dp) + .alignByBaseline(), + ) + + Column(modifier = Modifier.padding(start = Spacings.small).alignByBaseline()) { + content.title?.let { + Text( + it, + style = TextStyles.bodyMedium.copy( + fontWeight = FontWeight.Bold, + ), + modifier = Modifier.padding(bottom = Spacings.small), + ) + } + + Text(content.description) + } + } + } +} + +@ThemePreviews +@Composable +private fun SequentialOnboardingPreview() { + SpeziTheme(isPreview = true) { + SequentialOnboardingComposable( + title = "Title", + subtitle = "Subtitle", + content = listOf( + SequentialOnboardingContent( + title = "A thing to know", + description = "This is a first thing that you should know, read carefully!", + ), + SequentialOnboardingContent( + title = "Second thing to know", + description = "This is a second thing that you should know, read carefully!", + ), + SequentialOnboardingContent( + title = "Third thing to know", + description = "This is a third thing that you should know, read carefully!", + ), + ), + actionText = "Continue", + action = { + println("Done!") + } + ) + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/IllegalOnboardingStep.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/IllegalOnboardingStep.kt new file mode 100644 index 000000000..b6c809f7b --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/IllegalOnboardingStep.kt @@ -0,0 +1,11 @@ +package edu.stanford.spezi.module.onboarding.spezi.flow + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.component.StringResource.Companion.invoke + +@Composable +internal fun IllegalOnboardingStep() { + Text(StringResource("ILLEGAL_ONBOARDING_STEP").text()) +} From e3a1791973c26c4ecad96933cbbaa17d2174c966 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 10:55:42 -0800 Subject: [PATCH 38/51] Add current state of SpeziOnboarding --- .../onboarding/spezi/CustomOnboardingTest.kt | 33 +++++ .../onboarding/spezi/DynamicOnboardingTest.kt | 86 +++++++++++++ .../onboarding/spezi/OverallOnboardingTest.kt | 33 +++++ .../spezi/SequentialOnboardingTest.kt | 80 ++++++++++++ .../onboarding/spezi/WelcomeOnboardingTest.kt | 51 ++++++++ .../CustomOnboardingTestComposable.kt | 9 ++ .../DynamicOnboardingTestComposable.kt | 87 +++++++++++++ .../OverallOnboardingTestComposable.kt | 9 ++ .../SequentialOnboardingTestComposable.kt | 49 ++++++++ .../WelcomeOnboardingTestComposable.kt | 58 +++++++++ .../CustomOnboardingTestSimulator.kt | 11 ++ .../DynamicOnboardingTestSimulator.kt | 29 +++++ .../OverallOnboardingTestSimulator.kt | 11 ++ .../SequentialOnboardingTestSimulator.kt | 29 +++++ .../WelcomeOnboardingTestSimulator.kt | 23 ++++ .../spezi/SequentialOnboardingComposable.kt | 4 +- .../spezi/flow/OnboardingNavigationPath.kt | 61 +++++++++ .../onboarding/spezi/flow/OnboardingStack.kt | 118 ++++++++++++++++++ .../spezi/flow/OnboardingStackBuilder.kt | 11 ++ 19 files changed, 790 insertions(+), 2 deletions(-) create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/DynamicOnboardingTest.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingTest.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/CustomOnboardingTestComposable.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/DynamicOnboardingTestComposable.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/SequentialOnboardingTestComposable.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/WelcomeOnboardingTestComposable.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingTestSimulator.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingTestSimulator.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingTestSimulator.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingTestSimulator.kt create mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingTestSimulator.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStackBuilder.kt diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt new file mode 100644 index 000000000..2756697b9 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt @@ -0,0 +1,33 @@ +package edu.stanford.spezi.module.onboarding.spezi + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.module.onboarding.spezi.composables.CustomOnboardingTestComposable +import edu.stanford.spezi.module.onboarding.spezi.simulators.CustomOnboardingSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class CustomOnboardingTest { + + @get:Rule + val composeRule = createComposeRule() + + @Before + fun init() { + composeRule.setContent { + CustomOnboardingTestComposable() + } + } + + @Test + fun test() { + customOnboarding { + + } + } + + private fun customOnboarding(block: CustomOnboardingSimulator.() -> Unit) { + CustomOnboardingSimulator(composeRule).apply { block() } + } + +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/DynamicOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/DynamicOnboardingTest.kt new file mode 100644 index 000000000..49192e84f --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/DynamicOnboardingTest.kt @@ -0,0 +1,86 @@ +package edu.stanford.spezi.module.onboarding.spezi + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.module.onboarding.spezi.composables.DynamicOnboardingTestComposable +import edu.stanford.spezi.module.onboarding.spezi.simulators.DynamicOnboardingSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class DynamicOnboardingTest { + + @get:Rule + val composeRule = createComposeRule() + + @Before + fun init() { + composeRule.setContent { + DynamicOnboardingTestComposable() + } + } + + @Test + fun testDynamicOnboardingFlowONE() { + dynamicOnboarding { + assertTextExists("START") + clickButton("ONE") + assertTextExists("TITLE: ONE") + clickButton("Next") + composeRule.waitForIdle() + assertTextExists("TITLE: TWO") + clickButton("Next") + assertTextExists("Done") + assertTextExists("Dynamic Onboarding done!") + } + } + + @Test + fun testDynamicOnboardingFlowTWO() { + dynamicOnboarding { + assertTextExists("START") + clickButton("TWO") + assertTextExists("TITLE: TWO") + clickButton("Next") + composeRule.waitForIdle() + assertTextExists("Done") + assertTextExists("Dynamic Onboarding done!") + } + } + + @Test + fun testDynamicOnboardingFlowTHREE() { + dynamicOnboarding { + assertTextExists("START") + clickButton("THREE") + assertTextExists("TITLE: THREE") + clickButton("Next") + composeRule.waitForIdle() + assertTextExists("TITLE: ONE") + clickButton("Next") + assertTextExists("TITLE: TWO") + clickButton("Next") + assertTextExists("Done") + assertTextExists("Dynamic Onboarding done!") + } + } + + @Test + fun testDynamicOnboardingFlowNext() { + dynamicOnboarding { + assertTextExists("START") + clickButton("Next") + composeRule.waitForIdle() + assertTextExists("TITLE: ONE") + clickButton("Next") + composeRule.waitForIdle() + assertTextExists("TITLE: TWO") + clickButton("Next") + assertTextExists("Done") + assertTextExists("Dynamic Onboarding done!") + } + } + + private fun dynamicOnboarding(block: DynamicOnboardingSimulator.() -> Unit) { + DynamicOnboardingSimulator(composeRule).apply { block() } + } +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt new file mode 100644 index 000000000..08a58d60b --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt @@ -0,0 +1,33 @@ +package edu.stanford.spezi.module.onboarding.spezi + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.module.onboarding.spezi.composables.OverallOnboardingTestComposable +import edu.stanford.spezi.module.onboarding.spezi.simulators.OverallOnboardingSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class OverallOnboardingTest { + + @get:Rule + val composeRule = createComposeRule() + + @Before + fun init() { + composeRule.setContent { + OverallOnboardingTestComposable() + } + } + + @Test + fun test() { + overallOnboarding { + + } + } + + private fun overallOnboarding(block: OverallOnboardingSimulator.() -> Unit) { + OverallOnboardingSimulator(composeRule).apply { block() } + } + +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingTest.kt new file mode 100644 index 000000000..99fe10606 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingTest.kt @@ -0,0 +1,80 @@ +package edu.stanford.spezi.module.onboarding.spezi + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.module.onboarding.spezi.composables.SequentialOnboardingTestComposable +import edu.stanford.spezi.module.onboarding.spezi.simulators.SequentialOnboardingSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class SequentialOnboardingTest { + + @get:Rule + val composeRule = createComposeRule() + + @Before + fun init() { + composeRule.setContent { + SequentialOnboardingTestComposable() + } + } + + @Test + fun testSequentialOnboarding() { + sequentialOnboarding { + assertTextExists("Things to know") + assertTextExists("And you should pay close attention ...") + + assertTextExists("1") + assertTextExists("A thing to know") + assertTextDoesNotExist("2") + assertTextDoesNotExist("Second thing to know") + assertTextDoesNotExist("3") + assertTextDoesNotExist("Third thing to know") + assertTextDoesNotExist("4") + assertTextDoesNotExist("Now you should know all the things!") + + clickButton("Next") + + assertTextExists("1") + assertTextExists("A thing to know") + assertTextExists("2") + assertTextExists("Second thing to know") + assertTextDoesNotExist("3") + assertTextDoesNotExist("Third thing to know") + assertTextDoesNotExist("4") + assertTextDoesNotExist("Now you should know all the things!") + + clickButton("Next") + + assertTextExists("1") + assertTextExists("A thing to know") + assertTextExists("2") + assertTextExists("Second thing to know") + assertTextExists("3") + assertTextExists("Third thing to know") + assertTextDoesNotExist("4") + assertTextDoesNotExist("Now you should know all the things!") + + clickButton("Next") + + assertTextExists("1") + assertTextExists("A thing to know") + assertTextExists("2") + assertTextExists("Second thing to know") + assertTextExists("3") + assertTextExists("Third thing to know") + assertTextExists("4") + assertTextExists("Now you should know all the things!") + + clickButton("Continue") + + assertTextExists("Done") + assertTextExists("Sequential Onboarding done!") + } + } + + private fun sequentialOnboarding(block: SequentialOnboardingSimulator.() -> Unit) { + SequentialOnboardingSimulator(composeRule).apply { block() } + } +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt new file mode 100644 index 000000000..bece22a91 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt @@ -0,0 +1,51 @@ +package edu.stanford.spezi.module.onboarding.spezi + +import androidx.compose.ui.test.junit4.createComposeRule +import edu.stanford.spezi.module.onboarding.spezi.composables.WelcomeOnboardingTestComposable +import edu.stanford.spezi.module.onboarding.spezi.simulators.WelcomeOnboardingSimulator +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class WelcomeOnboardingTest { + + @get:Rule + val composeRule = createComposeRule() + + @Before + fun init() { + composeRule.setContent { + WelcomeOnboardingTestComposable() + } + } + + @Test + fun testWelcomeOnboarding() { + welcomeOnboarding { + assertTextExists("Welcome") + assertTextExists("Spezi UI Tests") + + assertTextExists("Tortoise") + assertTextExists("A Tortoise!") + + assertTextExists("Tree") + assertTextExists("A Tree!") + + assertTextExists("Letter") + assertTextExists("A letter!") + + assertTextExists("Circle") + assertTextExists("A circle!") + + clickButton("Learn More") + + assertTextExists("Done") + assertTextExists("Welcome Onboarding done!") + } + } + + private fun welcomeOnboarding(block: WelcomeOnboardingSimulator.() -> Unit) { + WelcomeOnboardingSimulator(composeRule).apply { block() } + } + +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/CustomOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/CustomOnboardingTestComposable.kt new file mode 100644 index 000000000..a1cae8598 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/CustomOnboardingTestComposable.kt @@ -0,0 +1,9 @@ +package edu.stanford.spezi.module.onboarding.spezi.composables + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +@Composable +fun CustomOnboardingTestComposable() { + Text("") +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/DynamicOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/DynamicOnboardingTestComposable.kt new file mode 100644 index 000000000..1c8256bf9 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/DynamicOnboardingTestComposable.kt @@ -0,0 +1,87 @@ +package edu.stanford.spezi.module.onboarding.spezi.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.module.onboarding.core.OnboardingTitle +import edu.stanford.spezi.module.onboarding.spezi.flow.LocalOnboardingNavigationPath +import edu.stanford.spezi.module.onboarding.spezi.flow.OnboardingStack + +object DynamicOnboardingTestStepId { + const val START = "START" + const val ONE = "ONE" + const val TWO = "TWO" + const val DONE = "DONE" +} + +@Composable +fun DynamicOnboardingTestComposable() { + OnboardingStack { + step(DynamicOnboardingTestStepId.START) { + val path = LocalOnboardingNavigationPath.current + + Column { + Text("START") + + Button(onClick = { + path.append(DynamicOnboardingTestStepId.ONE) + }) { + Text("ONE") + } + + Button(onClick = { + path.append(DynamicOnboardingTestStepId.TWO) + }) { + Text("TWO") + } + + Button(onClick = { + path.append { + DynamicOnboardingComposable("THREE") + } + }) { + Text("THREE") + } + + Button(onClick = { + path.nextStep() + }) { + Text("Next") + } + } + } + + step(DynamicOnboardingTestStepId.ONE) { + DynamicOnboardingComposable(DynamicOnboardingTestStepId.ONE) + } + + step(DynamicOnboardingTestStepId.TWO) { + DynamicOnboardingComposable(DynamicOnboardingTestStepId.TWO) + } + + step(DynamicOnboardingTestStepId.DONE) { + OnboardingTitle( + "Done", + "Dynamic Onboarding done!" + ) + } + } +} + +@Composable +private fun DynamicOnboardingComposable(title: String) { + val path = LocalOnboardingNavigationPath.current + + Column { + Text("TITLE: $title") + Button(onClick = { + path.nextStep() + }) { + Text("Next") + } + } +} + diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt new file mode 100644 index 000000000..9817482ce --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt @@ -0,0 +1,9 @@ +package edu.stanford.spezi.module.onboarding.spezi.composables + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +@Composable +fun OverallOnboardingTestComposable() { + Text("") +} \ No newline at end of file diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/SequentialOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/SequentialOnboardingTestComposable.kt new file mode 100644 index 000000000..787e6f691 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/SequentialOnboardingTestComposable.kt @@ -0,0 +1,49 @@ +package edu.stanford.spezi.module.onboarding.spezi.composables + +import androidx.compose.runtime.Composable +import edu.stanford.spezi.module.onboarding.core.OnboardingTitle +import edu.stanford.spezi.module.onboarding.spezi.SequentialOnboardingComposable +import edu.stanford.spezi.module.onboarding.spezi.SequentialOnboardingContent +import edu.stanford.spezi.module.onboarding.spezi.flow.LocalOnboardingNavigationPath +import edu.stanford.spezi.module.onboarding.spezi.flow.OnboardingStack + +@Composable +fun SequentialOnboardingTestComposable() { + OnboardingStack { + step("Welcome") { + val path = LocalOnboardingNavigationPath.current + SequentialOnboardingComposable( + title = "Things to know", + subtitle = "And you should pay close attention ...", + content = listOf( + SequentialOnboardingContent( + title = "A thing to know", + description = "This is a first thing that you should know, read carefully!" + ), + SequentialOnboardingContent( + title = "Second thing to know", + description = "This is a second thing that you should know, read carefully!" + ), + SequentialOnboardingContent( + title = "Third thing to know", + description = "This is a third thing that you should know, read carefully!" + ), + SequentialOnboardingContent( + description = "Now you should know all the things!" + ), + ), + actionText = "Continue", + action = { + path.nextStep() + } + ) + } + + step("Done") { + OnboardingTitle( + "Done", + "Sequential Onboarding done!" + ) + } + } +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/WelcomeOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/WelcomeOnboardingTestComposable.kt new file mode 100644 index 000000000..196cdcc76 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/WelcomeOnboardingTestComposable.kt @@ -0,0 +1,58 @@ +package edu.stanford.spezi.module.onboarding.spezi.composables + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Done +import androidx.compose.runtime.Composable +import edu.stanford.spezi.core.design.component.ImageResource +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.module.onboarding.core.OnboardingTitle +import edu.stanford.spezi.module.onboarding.spezi.OnboardingComposable +import edu.stanford.spezi.module.onboarding.spezi.OnboardingInformationContent +import edu.stanford.spezi.module.onboarding.spezi.flow.LocalOnboardingNavigationPath +import edu.stanford.spezi.module.onboarding.spezi.flow.OnboardingStack + +@Composable +fun WelcomeOnboardingTestComposable() { + OnboardingStack { + step("Welcome") { + val path = LocalOnboardingNavigationPath.current + OnboardingComposable( + title = "Welcome", + subtitle = "Spezi UI Tests", + areas = listOf( + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Done, StringResource("Icon")), + title = "Tortoise", + description = "A Tortoise!" + ), + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Done, StringResource("Icon")), + title = "Tree", + description = "A Tree!" + ), + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Done, StringResource("Icon")), + title = "Letter", + description = "A letter!" + ), + OnboardingInformationContent( + icon = ImageResource.Vector(Icons.Default.Done, StringResource("Icon")), + title = "Circle", + description = "A circle!" + ), + ), + actionText = "Learn More", + action = { + path.nextStep() + } + ) + } + + step("Done") { + OnboardingTitle( + "Done", + "Welcome Onboarding done!" + ) + } + } +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingTestSimulator.kt new file mode 100644 index 000000000..5d8d4e2bc --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingTestSimulator.kt @@ -0,0 +1,11 @@ +package edu.stanford.spezi.module.onboarding.spezi.simulators + +import androidx.compose.ui.test.junit4.ComposeTestRule + +class CustomOnboardingSimulator( + private val composeTestRule: ComposeTestRule +) { + suspend fun awaitIdle() { + composeTestRule.awaitIdle() + } +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingTestSimulator.kt new file mode 100644 index 000000000..e50542181 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingTestSimulator.kt @@ -0,0 +1,29 @@ +package edu.stanford.spezi.module.onboarding.spezi.simulators + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class DynamicOnboardingSimulator( + private val composeTestRule: ComposeTestRule +) { + fun assertTextExists(text: String) { + composeTestRule + .onNodeWithText(text) + .assertExists() + } + + fun assertTextDoesNotExist(text: String) { + composeTestRule + .onNodeWithText(text) + .assertDoesNotExist() + } + + fun clickButton(text: String) { + composeTestRule + .onNodeWithText(text) + .assertHasClickAction() + .performClick() + } +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingTestSimulator.kt new file mode 100644 index 000000000..ddafda58e --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingTestSimulator.kt @@ -0,0 +1,11 @@ +package edu.stanford.spezi.module.onboarding.spezi.simulators + +import androidx.compose.ui.test.junit4.ComposeTestRule + +class OverallOnboardingSimulator( + private val composeTestRule: ComposeTestRule +) { + suspend fun awaitIdle() { + composeTestRule.awaitIdle() + } +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingTestSimulator.kt new file mode 100644 index 000000000..244dfd1f3 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingTestSimulator.kt @@ -0,0 +1,29 @@ +package edu.stanford.spezi.module.onboarding.spezi.simulators + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class SequentialOnboardingSimulator( + private val composeTestRule: ComposeTestRule +) { + fun assertTextExists(text: String) { + composeTestRule + .onNodeWithText(text) + .assertExists() + } + + fun assertTextDoesNotExist(text: String) { + composeTestRule + .onNodeWithText(text) + .assertDoesNotExist() + } + + fun clickButton(text: String) { + composeTestRule + .onNodeWithText(text) + .assertHasClickAction() + .performClick() + } +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingTestSimulator.kt new file mode 100644 index 000000000..b988a0c71 --- /dev/null +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingTestSimulator.kt @@ -0,0 +1,23 @@ +package edu.stanford.spezi.module.onboarding.spezi.simulators + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class WelcomeOnboardingSimulator( + private val composeTestRule: ComposeTestRule +) { + fun assertTextExists(text: String) { + composeTestRule + .onNodeWithText(text) + .assertExists() + } + + fun clickButton(text: String) { + composeTestRule + .onNodeWithText(text) + .assertHasClickAction() + .performClick() + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingComposable.kt index 87c01bd8f..37fd964d6 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/SequentialOnboardingComposable.kt @@ -24,7 +24,7 @@ import edu.stanford.spezi.module.onboarding.core.OnboardingComposable import edu.stanford.spezi.module.onboarding.core.OnboardingTitle data class SequentialOnboardingContent( - val title: String?, + val title: String? = null, val description: String, ) @@ -69,7 +69,7 @@ fun SequentialOnboardingComposable( } }, action = { - val isDone = currentContentIndex.intValue >= content.size + val isDone = currentContentIndex.intValue >= content.size - 1 OnboardingActions( primaryText = if (isDone) actionText else StringResource("Next").text(), primaryAction = { diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt new file mode 100644 index 000000000..6a125d23d --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt @@ -0,0 +1,61 @@ +package edu.stanford.spezi.module.onboarding.spezi.flow + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.navOptions +import edu.stanford.spezi.core.utils.UUID + +class OnboardingNavigationPath internal constructor( + internal val navController: NavController, + internal var steps: List, +) { + internal val customSteps = mutableListOf() + + private var currentOnboardingStep = + navController.backQueue + .firstOrNull { pathItem -> steps.any { pathItem.id == it.id } }?.id + ?: steps.firstOrNull()?.id + + fun nextStep() { + val currentOnboardingStepId = currentOnboardingStep + val currentStepIndex = steps.indexOfFirst { it.id == currentOnboardingStepId } + if (currentStepIndex < 0 || currentStepIndex + 1 >= steps.size) { + return + } + + navController.navigate( + route = steps[currentStepIndex + 1].id, + navOptions = navOptions { + // TODO: Think about what to inject here + }, + navigatorExtras = null, // TODO: Think about what to inject here + ) + } + + fun append(id: String) { + val step = steps.firstOrNull { it.id == id } ?: error("") + + navController.navigate( + route = step.id, + navOptions = navOptions { + // TODO: Think about what to inject here + }, + navigatorExtras = null, // TODO: Think about what to inject here + ) + } + + fun append(content: @Composable () -> Unit) { + val stepId = UUID().toString() + customSteps.add(OnboardingNavigationStep(stepId, content)) + navController.navigate( + route = stepId, + navOptions = navOptions { + // TODO: Think about what to inject here + }, + ) + } + + fun removeLast() { + navController.navigateUp() + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt new file mode 100644 index 000000000..5d3aecb1d --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt @@ -0,0 +1,118 @@ +package edu.stanford.spezi.module.onboarding.spezi.flow + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraph +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.createGraph +import edu.stanford.spezi.core.design.component.Button +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews + +internal data class OnboardingNavigationStep( + val id: String, + val content: @Composable () -> Unit, +) + +val LocalOnboardingNavigationPath = compositionLocalOf { + error("Do not access this outside of OnboardingStack!") +} + +@Composable +fun OnboardingStack( + modifier: Modifier = Modifier, + onComplete: () -> Unit = {}, + startAtStep: String? = null, + content: OnboardingStackBuilder.() -> Unit, +) { + val steps = OnboardingStackBuilder().apply { content() }.steps + val navController = rememberNavController() + + val startDestination = remember(startAtStep, steps) { + startAtStep ?: steps.firstOrNull()?.id ?: error("No step specified") + } + val navigationPath = remember { OnboardingNavigationPath(navController, steps) } + + val navGraph = remember(startDestination, steps, content) { + navController.createGraph(startDestination) { + for (step in navigationPath.customSteps) { + composable(step.id) { + CompositionLocalProvider(LocalOnboardingNavigationPath provides navigationPath) { + step.content() + } + } + } + + for (step in steps) { + composable(step.id) { + CompositionLocalProvider(LocalOnboardingNavigationPath provides navigationPath) { + step.content() + } + } + } + } + } + + NavHost( + navController = navController, + graph = navGraph, + modifier = modifier, + ) + + LaunchedEffect(navController) { + navController.currentBackStackEntryFlow.collect { entry -> + if (entry.destination.route == null) { + onComplete() + } + } + } +} + +@Composable +private fun OnboardingStepPreview(index: Int) { + val path = LocalOnboardingNavigationPath.current + + Column { + Text("Page $index") + + Button(onClick = { + path.removeLast() + }) { + Text("Remove Last") + } + + Button(onClick = { + path.nextStep() + }) { + Text("Next") + } + } +} + +@ThemePreviews +@Composable +private fun OnboardingStackPreview() { + SpeziTheme(isPreview = true) { + OnboardingStack { + step("1") { + OnboardingStepPreview(1) + } + step("2") { + OnboardingStepPreview(2) + } + step("3") { + @Suppress("detekt:MagicNumber") + OnboardingStepPreview(3) + } + } + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStackBuilder.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStackBuilder.kt new file mode 100644 index 000000000..0d1d6b205 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStackBuilder.kt @@ -0,0 +1,11 @@ +package edu.stanford.spezi.module.onboarding.spezi.flow + +import androidx.compose.runtime.Composable + +class OnboardingStackBuilder internal constructor() { + internal val steps = mutableListOf() + + fun step(id: String, content: @Composable () -> Unit) { + steps.add(OnboardingNavigationStep(id, content)) + } +} \ No newline at end of file From 3e83e41414f98297c278ce71d30cd3f02d1a9a7b Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 13:56:14 -0800 Subject: [PATCH 39/51] detekt --- .../spezi/module/onboarding/spezi/CustomOnboardingTest.kt | 2 -- .../spezi/module/onboarding/spezi/OverallOnboardingTest.kt | 2 -- .../spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt | 1 - .../spezi/composables/DynamicOnboardingTestComposable.kt | 3 --- .../spezi/composables/OverallOnboardingTestComposable.kt | 2 +- ...OnboardingTestSimulator.kt => CustomOnboardingSimulator.kt} | 2 +- ...nboardingTestSimulator.kt => DynamicOnboardingSimulator.kt} | 2 +- ...nboardingTestSimulator.kt => OverallOnboardingSimulator.kt} | 2 +- ...ardingTestSimulator.kt => SequentialOnboardingSimulator.kt} | 2 +- ...nboardingTestSimulator.kt => WelcomeOnboardingSimulator.kt} | 2 +- .../spezi/module/onboarding/spezi/flow/OnboardingStack.kt | 2 -- .../module/onboarding/spezi/flow/OnboardingStackBuilder.kt | 2 +- 12 files changed, 7 insertions(+), 17 deletions(-) rename modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/{CustomOnboardingTestSimulator.kt => CustomOnboardingSimulator.kt} (82%) rename modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/{DynamicOnboardingTestSimulator.kt => DynamicOnboardingSimulator.kt} (93%) rename modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/{OverallOnboardingTestSimulator.kt => OverallOnboardingSimulator.kt} (82%) rename modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/{SequentialOnboardingTestSimulator.kt => SequentialOnboardingSimulator.kt} (93%) rename modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/{WelcomeOnboardingTestSimulator.kt => WelcomeOnboardingSimulator.kt} (92%) diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt index 2756697b9..91e5e89f5 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt @@ -22,12 +22,10 @@ class CustomOnboardingTest { @Test fun test() { customOnboarding { - } } private fun customOnboarding(block: CustomOnboardingSimulator.() -> Unit) { CustomOnboardingSimulator(composeRule).apply { block() } } - } diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt index 08a58d60b..505d177b9 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt @@ -22,12 +22,10 @@ class OverallOnboardingTest { @Test fun test() { overallOnboarding { - } } private fun overallOnboarding(block: OverallOnboardingSimulator.() -> Unit) { OverallOnboardingSimulator(composeRule).apply { block() } } - } diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt index bece22a91..d94a88912 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/WelcomeOnboardingTest.kt @@ -47,5 +47,4 @@ class WelcomeOnboardingTest { private fun welcomeOnboarding(block: WelcomeOnboardingSimulator.() -> Unit) { WelcomeOnboardingSimulator(composeRule).apply { block() } } - } diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/DynamicOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/DynamicOnboardingTestComposable.kt index 1c8256bf9..79fef653e 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/DynamicOnboardingTestComposable.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/DynamicOnboardingTestComposable.kt @@ -4,8 +4,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import edu.stanford.spezi.module.onboarding.core.OnboardingTitle import edu.stanford.spezi.module.onboarding.spezi.flow.LocalOnboardingNavigationPath import edu.stanford.spezi.module.onboarding.spezi.flow.OnboardingStack @@ -84,4 +82,3 @@ private fun DynamicOnboardingComposable(title: String) { } } } - diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt index 9817482ce..c2356024f 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt @@ -6,4 +6,4 @@ import androidx.compose.runtime.Composable @Composable fun OverallOnboardingTestComposable() { Text("") -} \ No newline at end of file +} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingSimulator.kt similarity index 82% rename from modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingTestSimulator.kt rename to modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingSimulator.kt index 5d8d4e2bc..6d9c322d8 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingTestSimulator.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingSimulator.kt @@ -3,7 +3,7 @@ package edu.stanford.spezi.module.onboarding.spezi.simulators import androidx.compose.ui.test.junit4.ComposeTestRule class CustomOnboardingSimulator( - private val composeTestRule: ComposeTestRule + private val composeTestRule: ComposeTestRule, ) { suspend fun awaitIdle() { composeTestRule.awaitIdle() diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingSimulator.kt similarity index 93% rename from modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingTestSimulator.kt rename to modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingSimulator.kt index e50542181..c46ccaafd 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingTestSimulator.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/DynamicOnboardingSimulator.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick class DynamicOnboardingSimulator( - private val composeTestRule: ComposeTestRule + private val composeTestRule: ComposeTestRule, ) { fun assertTextExists(text: String) { composeTestRule diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingSimulator.kt similarity index 82% rename from modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingTestSimulator.kt rename to modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingSimulator.kt index ddafda58e..d365ebdee 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingTestSimulator.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingSimulator.kt @@ -3,7 +3,7 @@ package edu.stanford.spezi.module.onboarding.spezi.simulators import androidx.compose.ui.test.junit4.ComposeTestRule class OverallOnboardingSimulator( - private val composeTestRule: ComposeTestRule + private val composeTestRule: ComposeTestRule, ) { suspend fun awaitIdle() { composeTestRule.awaitIdle() diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingSimulator.kt similarity index 93% rename from modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingTestSimulator.kt rename to modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingSimulator.kt index 244dfd1f3..73856e24d 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingTestSimulator.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/SequentialOnboardingSimulator.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick class SequentialOnboardingSimulator( - private val composeTestRule: ComposeTestRule + private val composeTestRule: ComposeTestRule, ) { fun assertTextExists(text: String) { composeTestRule diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingTestSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingSimulator.kt similarity index 92% rename from modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingTestSimulator.kt rename to modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingSimulator.kt index b988a0c71..8e1a19f6b 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingTestSimulator.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/WelcomeOnboardingSimulator.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick class WelcomeOnboardingSimulator( - private val composeTestRule: ComposeTestRule + private val composeTestRule: ComposeTestRule, ) { fun assertTextExists(text: String) { composeTestRule diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt index 5d3aecb1d..d2ee22632 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt @@ -8,8 +8,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.navigation.NavGraph -import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStackBuilder.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStackBuilder.kt index 0d1d6b205..2e88a6df9 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStackBuilder.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStackBuilder.kt @@ -8,4 +8,4 @@ class OnboardingStackBuilder internal constructor() { fun step(id: String, content: @Composable () -> Unit) { steps.add(OnboardingNavigationStep(id, content)) } -} \ No newline at end of file +} From e34fccde48aa4893be80d9ae44d94f262406ec5c Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 14:56:35 -0800 Subject: [PATCH 40/51] Update OnboardingTests --- .../onboarding/spezi/DynamicOnboardingTest.kt | 4 -- .../spezi/flow/OnboardingNavigationPath.kt | 70 ++++++++++++++----- .../onboarding/spezi/flow/OnboardingStack.kt | 33 +++------ 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/DynamicOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/DynamicOnboardingTest.kt index 49192e84f..ca2241fb7 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/DynamicOnboardingTest.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/DynamicOnboardingTest.kt @@ -26,7 +26,6 @@ class DynamicOnboardingTest { clickButton("ONE") assertTextExists("TITLE: ONE") clickButton("Next") - composeRule.waitForIdle() assertTextExists("TITLE: TWO") clickButton("Next") assertTextExists("Done") @@ -41,7 +40,6 @@ class DynamicOnboardingTest { clickButton("TWO") assertTextExists("TITLE: TWO") clickButton("Next") - composeRule.waitForIdle() assertTextExists("Done") assertTextExists("Dynamic Onboarding done!") } @@ -54,7 +52,6 @@ class DynamicOnboardingTest { clickButton("THREE") assertTextExists("TITLE: THREE") clickButton("Next") - composeRule.waitForIdle() assertTextExists("TITLE: ONE") clickButton("Next") assertTextExists("TITLE: TWO") @@ -72,7 +69,6 @@ class DynamicOnboardingTest { composeRule.waitForIdle() assertTextExists("TITLE: ONE") clickButton("Next") - composeRule.waitForIdle() assertTextExists("TITLE: TWO") clickButton("Next") assertTextExists("Done") diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt index 6a125d23d..7fdebc404 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt @@ -1,23 +1,34 @@ package edu.stanford.spezi.module.onboarding.spezi.flow import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.navigation.NavController +import androidx.navigation.NavGraph +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.createGraph +import androidx.navigation.navArgument import androidx.navigation.navOptions import edu.stanford.spezi.core.utils.UUID class OnboardingNavigationPath internal constructor( internal val navController: NavController, + internal var startDestination: String, internal var steps: List, ) { - internal val customSteps = mutableListOf() + private val customSteps = mutableListOf() - private var currentOnboardingStep = - navController.backQueue - .firstOrNull { pathItem -> steps.any { pathItem.id == it.id } }?.id - ?: steps.firstOrNull()?.id + private val currentOnboardingStepId: String? get() { + val currentQueue = navController.backQueue + val lastStep = currentQueue.lastOrNull { queueItem -> + !(queueItem.destination.route ?: "").startsWith(customRoute("")) + } + + return lastStep?.destination?.route ?: steps.firstOrNull()?.id + } fun nextStep() { - val currentOnboardingStepId = currentOnboardingStep + val currentOnboardingStepId = currentOnboardingStepId val currentStepIndex = steps.indexOfFirst { it.id == currentOnboardingStepId } if (currentStepIndex < 0 || currentStepIndex + 1 >= steps.size) { return @@ -25,10 +36,8 @@ class OnboardingNavigationPath internal constructor( navController.navigate( route = steps[currentStepIndex + 1].id, - navOptions = navOptions { - // TODO: Think about what to inject here - }, - navigatorExtras = null, // TODO: Think about what to inject here + navOptions = navOptions {}, // TODO: Anything important/relevant needed here? + navigatorExtras = null, // TODO: Anything important/relevant needed here? ) } @@ -37,10 +46,8 @@ class OnboardingNavigationPath internal constructor( navController.navigate( route = step.id, - navOptions = navOptions { - // TODO: Think about what to inject here - }, - navigatorExtras = null, // TODO: Think about what to inject here + navOptions = navOptions {}, // TODO: Anything important/relevant needed here? + navigatorExtras = null, // TODO: Anything important/relevant needed here? ) } @@ -48,14 +55,41 @@ class OnboardingNavigationPath internal constructor( val stepId = UUID().toString() customSteps.add(OnboardingNavigationStep(stepId, content)) navController.navigate( - route = stepId, - navOptions = navOptions { - // TODO: Think about what to inject here - }, + route = customRoute(stepId), + navOptions = navOptions {}, // TODO: Anything important/relevant needed here? + navigatorExtras = null, // TODO: Anything important/relevant needed here? ) } fun removeLast() { navController.navigateUp() } + + internal fun createGraph(): NavGraph { + println("Recreating graph") + + val path = this + return navController.createGraph(startDestination) { + composable( + route = customRoute("{id}"), + arguments = listOf(navArgument("id") { type = NavType.StringType }) + ) { backStackEntry -> + val stepId = backStackEntry.arguments?.getString("id") ?: error("Unknown custom route") + val step = customSteps.firstOrNull { it.id == stepId } ?: error("Unknown custom route") + CompositionLocalProvider(LocalOnboardingNavigationPath provides path) { + step.content() + } + } + + for (step in steps) { + composable(step.id) { + CompositionLocalProvider(LocalOnboardingNavigationPath provides path) { + step.content() + } + } + } + } + } + + private fun customRoute(id: String) = "custom/$id" } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt index d2ee22632..33849f258 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingStack.kt @@ -3,13 +3,11 @@ package edu.stanford.spezi.module.onboarding.spezi.flow import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.createGraph import edu.stanford.spezi.core.design.component.Button @@ -34,35 +32,26 @@ fun OnboardingStack( ) { val steps = OnboardingStackBuilder().apply { content() }.steps val navController = rememberNavController() - val startDestination = remember(startAtStep, steps) { startAtStep ?: steps.firstOrNull()?.id ?: error("No step specified") } - val navigationPath = remember { OnboardingNavigationPath(navController, steps) } + val navigationPath = remember { + OnboardingNavigationPath(navController, startDestination, steps) + } - val navGraph = remember(startDestination, steps, content) { - navController.createGraph(startDestination) { - for (step in navigationPath.customSteps) { - composable(step.id) { - CompositionLocalProvider(LocalOnboardingNavigationPath provides navigationPath) { - step.content() - } - } - } + LaunchedEffect(startDestination) { + navigationPath.startDestination = startDestination + } - for (step in steps) { - composable(step.id) { - CompositionLocalProvider(LocalOnboardingNavigationPath provides navigationPath) { - step.content() - } - } - } - } + LaunchedEffect(steps) { + navigationPath.steps = steps } NavHost( navController = navController, - graph = navGraph, + graph = remember(startDestination, steps) { + navigationPath.createGraph() + }, modifier = modifier, ) From 758b44c1ee1be546c4a4a3b792bdae6244a35c07 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 14:59:26 -0800 Subject: [PATCH 41/51] Use IllegalOnboardingStep --- .../onboarding/spezi/flow/OnboardingNavigationPath.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt index 7fdebc404..6e2f041a0 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/flow/OnboardingNavigationPath.kt @@ -74,10 +74,11 @@ class OnboardingNavigationPath internal constructor( route = customRoute("{id}"), arguments = listOf(navArgument("id") { type = NavType.StringType }) ) { backStackEntry -> - val stepId = backStackEntry.arguments?.getString("id") ?: error("Unknown custom route") - val step = customSteps.firstOrNull { it.id == stepId } ?: error("Unknown custom route") + val step = backStackEntry.arguments?.getString("id")?.let { stepId -> + customSteps.firstOrNull { it.id == stepId } + } CompositionLocalProvider(LocalOnboardingNavigationPath provides path) { - step.content() + step?.content() ?: IllegalOnboardingStep() } } From 3ca3568713f2234b6e4b660d5798de94e24a4152 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 15:01:19 -0800 Subject: [PATCH 42/51] Remove empty tests --- .../onboarding/spezi/CustomOnboardingTest.kt | 31 ------------------- .../onboarding/spezi/OverallOnboardingTest.kt | 31 ------------------- .../CustomOnboardingTestComposable.kt | 9 ------ .../OverallOnboardingTestComposable.kt | 9 ------ .../simulators/CustomOnboardingSimulator.kt | 11 ------- .../simulators/OverallOnboardingSimulator.kt | 11 ------- 6 files changed, 102 deletions(-) delete mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt delete mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt delete mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/CustomOnboardingTestComposable.kt delete mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt delete mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingSimulator.kt delete mode 100644 modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingSimulator.kt diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt deleted file mode 100644 index 91e5e89f5..000000000 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/CustomOnboardingTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package edu.stanford.spezi.module.onboarding.spezi - -import androidx.compose.ui.test.junit4.createComposeRule -import edu.stanford.spezi.module.onboarding.spezi.composables.CustomOnboardingTestComposable -import edu.stanford.spezi.module.onboarding.spezi.simulators.CustomOnboardingSimulator -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -class CustomOnboardingTest { - - @get:Rule - val composeRule = createComposeRule() - - @Before - fun init() { - composeRule.setContent { - CustomOnboardingTestComposable() - } - } - - @Test - fun test() { - customOnboarding { - } - } - - private fun customOnboarding(block: CustomOnboardingSimulator.() -> Unit) { - CustomOnboardingSimulator(composeRule).apply { block() } - } -} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt deleted file mode 100644 index 505d177b9..000000000 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/OverallOnboardingTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package edu.stanford.spezi.module.onboarding.spezi - -import androidx.compose.ui.test.junit4.createComposeRule -import edu.stanford.spezi.module.onboarding.spezi.composables.OverallOnboardingTestComposable -import edu.stanford.spezi.module.onboarding.spezi.simulators.OverallOnboardingSimulator -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -class OverallOnboardingTest { - - @get:Rule - val composeRule = createComposeRule() - - @Before - fun init() { - composeRule.setContent { - OverallOnboardingTestComposable() - } - } - - @Test - fun test() { - overallOnboarding { - } - } - - private fun overallOnboarding(block: OverallOnboardingSimulator.() -> Unit) { - OverallOnboardingSimulator(composeRule).apply { block() } - } -} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/CustomOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/CustomOnboardingTestComposable.kt deleted file mode 100644 index a1cae8598..000000000 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/CustomOnboardingTestComposable.kt +++ /dev/null @@ -1,9 +0,0 @@ -package edu.stanford.spezi.module.onboarding.spezi.composables - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable - -@Composable -fun CustomOnboardingTestComposable() { - Text("") -} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt deleted file mode 100644 index c2356024f..000000000 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/composables/OverallOnboardingTestComposable.kt +++ /dev/null @@ -1,9 +0,0 @@ -package edu.stanford.spezi.module.onboarding.spezi.composables - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable - -@Composable -fun OverallOnboardingTestComposable() { - Text("") -} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingSimulator.kt deleted file mode 100644 index 6d9c322d8..000000000 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/CustomOnboardingSimulator.kt +++ /dev/null @@ -1,11 +0,0 @@ -package edu.stanford.spezi.module.onboarding.spezi.simulators - -import androidx.compose.ui.test.junit4.ComposeTestRule - -class CustomOnboardingSimulator( - private val composeTestRule: ComposeTestRule, -) { - suspend fun awaitIdle() { - composeTestRule.awaitIdle() - } -} diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingSimulator.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingSimulator.kt deleted file mode 100644 index d365ebdee..000000000 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/spezi/simulators/OverallOnboardingSimulator.kt +++ /dev/null @@ -1,11 +0,0 @@ -package edu.stanford.spezi.module.onboarding.spezi.simulators - -import androidx.compose.ui.test.junit4.ComposeTestRule - -class OverallOnboardingSimulator( - private val composeTestRule: ComposeTestRule, -) { - suspend fun awaitIdle() { - composeTestRule.awaitIdle() - } -} From 3b9e3c2f9f59bf9b54f28b6bfaf395328de987f7 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 15:20:29 -0800 Subject: [PATCH 43/51] Update Consent --- .../component/markdown/MarkdownComposable.kt | 10 ------- .../component/markdown/MarkdownUiState.kt | 5 ---- .../component/markdown/MarkdownViewModel.kt | 29 ------------------- .../onboarding/consent/ConsentPdfService.kt | 1 + .../onboarding/consent/ConsentUiState.kt | 1 + .../onboarding/consent/ConsentViewModel.kt | 1 + .../{ => spezi}/consent/ConsentConstraint.kt | 2 +- .../{ => spezi}/consent/ConsentDataSource.kt | 2 +- .../{ => spezi}/consent/ConsentDocument.kt | 8 ++++- .../consent/ConsentDocumentExport.kt | 2 +- .../ConsentDocumentExportConfiguration.kt | 3 +- .../consent/OnboardingConsentComposable.kt | 11 +++++-- 12 files changed, 23 insertions(+), 52 deletions(-) delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt delete mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{ => spezi}/consent/ConsentConstraint.kt (71%) rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{ => spezi}/consent/ConsentDataSource.kt (92%) rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{ => spezi}/consent/ConsentDocument.kt (93%) rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{ => spezi}/consent/ConsentDocumentExport.kt (85%) rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{ => spezi}/consent/ConsentDocumentExportConfiguration.kt (90%) rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{ => spezi}/consent/OnboardingConsentComposable.kt (91%) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt index be2d1ae6d..a01503e04 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -15,15 +14,6 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles -@Composable -fun MarkdownComposable(data: suspend () -> ByteArray) { - // TODO: Figure out why hiltViewModel is not working and how one would do that anyways - val viewModel = remember { MarkdownViewModel(data, MarkdownParser()) } - val uiState = viewModel.uiState.collectAsState() - - MarkdownComponent(uiState.value.elements ?: emptyList()) -} - @Composable fun MarkdownComponent(markdownElements: List) { LazyColumn(modifier = Modifier.padding(Spacings.medium)) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt deleted file mode 100644 index d18650d1c..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package edu.stanford.spezi.core.design.component.markdown - -data class MarkdownUiState( - val elements: List? = null, -) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt deleted file mode 100644 index 97f2cc80e..000000000 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt +++ /dev/null @@ -1,29 +0,0 @@ -package edu.stanford.spezi.core.design.component.markdown - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.nio.charset.StandardCharsets - -internal class MarkdownViewModel @AssistedInject internal constructor( - @Assisted private val data: suspend () -> ByteArray, - private val markdownParser: MarkdownParser, -) : ViewModel() { - private val _uiState = MutableStateFlow(MarkdownUiState()) - val uiState: StateFlow = _uiState - - init { - viewModelScope.launch { - val markdownText = data().toString(StandardCharsets.UTF_8) - val markdownElements = markdownParser.parse(markdownText) - _uiState.update { - it.copy(elements = markdownElements) - } - } - } -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt index 727b2bc1a..1f87dabc5 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.asAndroidPath import edu.stanford.spezi.core.coroutines.di.Dispatching import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDocumentExportConfiguration import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import java.time.LocalDate diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt index 46d46bbe2..d5ca5fa98 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.graphics.Path import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDocumentExportConfiguration internal data class ConsentUiState( val name: PersonNameComponents = PersonNameComponents(), diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt index 56f4f8ff5..8fdd07564 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt @@ -3,6 +3,7 @@ package edu.stanford.spezi.module.onboarding.consent import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDataSource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentConstraint.kt similarity index 71% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentConstraint.kt index b5f4c9169..fef898547 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentConstraint.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.module.onboarding.consent +package edu.stanford.spezi.module.onboarding.spezi.consent import edu.stanford.spezi.core.utils.Standard diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDataSource.kt similarity index 92% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDataSource.kt index 28d38df0e..d46e52894 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDataSource.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.module.onboarding.consent +package edu.stanford.spezi.module.onboarding.spezi.consent import android.graphics.pdf.PdfDocument import edu.stanford.spezi.core.utils.Standard diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocument.kt similarity index 93% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocument.kt index d1ebafb20..431f1089c 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocument.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.module.onboarding.consent +package edu.stanford.spezi.module.onboarding.spezi.consent import android.annotation.SuppressLint import androidx.compose.foundation.layout.Column @@ -25,9 +25,15 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.component.StringResource.Companion.invoke import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.module.onboarding.consent.ConsentAction +import edu.stanford.spezi.module.onboarding.consent.ConsentUiState +import edu.stanford.spezi.module.onboarding.consent.ConsentViewState +import edu.stanford.spezi.module.onboarding.consent.SignatureCanvas +import edu.stanford.spezi.module.onboarding.consent.TextFieldType import java.nio.charset.StandardCharsets data class ConsentDocument( diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocumentExport.kt similarity index 85% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocumentExport.kt index 63a10e3ee..d3d6ab8ff 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocumentExport.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.module.onboarding.consent +package edu.stanford.spezi.module.onboarding.spezi.consent import android.graphics.pdf.PdfDocument diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExportConfiguration.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocumentExportConfiguration.kt similarity index 90% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExportConfiguration.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocumentExportConfiguration.kt index 8e21ac155..eb993bf54 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExportConfiguration.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocumentExportConfiguration.kt @@ -1,6 +1,7 @@ -package edu.stanford.spezi.module.onboarding.consent +package edu.stanford.spezi.module.onboarding.spezi.consent import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.component.StringResource.Companion.invoke data class ConsentDocumentExportConfiguration( val paperSize: PaperSize = PaperSize.usLetter, diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/OnboardingConsentComposable.kt similarity index 91% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/OnboardingConsentComposable.kt index 9b32122c2..ff4ed2729 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/OnboardingConsentComposable.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.module.onboarding.consent +package edu.stanford.spezi.module.onboarding.spezi.consent import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.layout.Column @@ -22,13 +22,18 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel import edu.stanford.spezi.core.design.component.Button import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.component.markdown.MarkdownComposable +import edu.stanford.spezi.core.design.component.StringResource.Companion.invoke import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.views.model.ViewState +import edu.stanford.spezi.core.design.views.views.views.text.MarkdownBytes import edu.stanford.spezi.core.utils.extensions.testIdentifier +import edu.stanford.spezi.module.onboarding.consent.ConsentAction +import edu.stanford.spezi.module.onboarding.consent.ConsentUiState +import edu.stanford.spezi.module.onboarding.consent.ConsentViewModel +import edu.stanford.spezi.module.onboarding.consent.ConsentViewState import edu.stanford.spezi.module.onboarding.core.OnboardingComposable import edu.stanford.spezi.module.onboarding.core.OnboardingTitle import kotlinx.coroutines.launch @@ -103,7 +108,7 @@ internal fun OnboardingConsentComposableContent( ) Column { Spacer(modifier = Modifier.height(Spacings.medium)) - MarkdownComposable(markdown) + MarkdownBytes(markdown) Spacer( modifier = Modifier .height(Spacings.small) From 602b79483b3371841b9d76d9045a3e81daa3f48e Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 15:21:56 -0800 Subject: [PATCH 44/51] Make it build --- .../bdh/engagehf/onboarding/EngageConsentManager.kt | 1 + .../bdh/engagehf/onboarding/OnboardingModule.kt | 1 + .../spezi/module/onboarding/consent/ConsentManager.kt | 11 +++++++++++ 3 files changed, 13 insertions(+) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt index 9e2233a07..784f71d44 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt @@ -3,6 +3,7 @@ package edu.stanford.bdh.engagehf.onboarding import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent import edu.stanford.spezi.core.navigation.Navigator import edu.stanford.spezi.core.utils.MessageNotifier +import edu.stanford.spezi.module.onboarding.consent.ConsentManager import javax.inject.Inject class EngageConsentManager @Inject internal constructor( diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt index 73edc50ff..a59c013e6 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt @@ -4,6 +4,7 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import edu.stanford.spezi.module.onboarding.consent.ConsentManager import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeRepository import edu.stanford.spezi.module.onboarding.onboarding.OnboardingRepository import edu.stanford.spezi.module.onboarding.sequential.SequentialOnboardingRepository diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt new file mode 100644 index 000000000..784a4ec24 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt @@ -0,0 +1,11 @@ +package edu.stanford.spezi.module.onboarding.consent + +/** + * A interface that needs to be implemented and provided by the app to provide the consent text and handle consent actions. + * @see edu.stanford.bdh.engagehf.onboarding.EngageConsentManager + */ +interface ConsentManager { + suspend fun getMarkdownText(): String + suspend fun onConsented() + suspend fun onConsentFailure(error: Throwable) +} From b8af94b221efa7db5e8d141b37a9c699eefcfbd6 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 15:23:17 -0800 Subject: [PATCH 45/51] Reduce changes --- .../spezi/core/design/component/markdown/MarkdownParser.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt index e28cbf74a..f5673f699 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt @@ -7,6 +7,7 @@ private const val HEADING_LEVEL_2 = 2 private const val HEADING_LEVEL_3 = 3 class MarkdownParser @Inject constructor() { + fun parse(text: String): List = buildList { text.lines().forEach { line -> when { From 4e6e3ee1e4249e26864d299badb1ffd5b6436159 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Sun, 24 Nov 2024 15:44:32 -0800 Subject: [PATCH 46/51] Bring back some old functionality --- .../onboarding/OnboardingNavigationEvent.kt | 1 + .../onboarding/consent/ConsentScreen.kt | 89 +++++++++++ .../onboarding/consent/ConsentUiState.kt | 1 + .../module/onboarding/consent/SignaturePad.kt | 141 ++++++++++++++++++ .../spezi/consent/ConsentDocument.kt | 2 - .../{ => spezi}/consent/ConsentViewState.kt | 2 +- .../consent/OnboardingConsentComposable.kt | 16 +- 7 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/{ => spezi}/consent/ConsentViewState.kt (89%) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt index cef78a7f1..084bb6644 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt @@ -3,6 +3,7 @@ package edu.stanford.spezi.module.onboarding import edu.stanford.spezi.core.navigation.NavigationEvent sealed class OnboardingNavigationEvent : NavigationEvent { + data object InvitationCodeScreen : OnboardingNavigationEvent() data class OnboardingScreen(val clearBackStack: Boolean) : OnboardingNavigationEvent() data object SequentialOnboardingScreen : OnboardingNavigationEvent() diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt new file mode 100644 index 000000000..0fa4a960f --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt @@ -0,0 +1,89 @@ +package edu.stanford.spezi.module.onboarding.consent + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.hilt.navigation.compose.hiltViewModel +import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent +import edu.stanford.spezi.core.design.component.markdown.MarkdownElement +import edu.stanford.spezi.core.design.theme.Spacings +import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.core.utils.extensions.testIdentifier + +@Composable +fun ConsentScreen() { + val viewModel: ConsentViewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsState() + + ConsentScreen( + onAction = viewModel::onAction, uiState = uiState + ) +} + +@Composable +private fun ConsentScreen( + uiState: ConsentUiState, + onAction: (ConsentAction) -> Unit, +) { + Column( + modifier = Modifier + .testIdentifier(ConsentScreenTestIdentifier.ROOT) + .fillMaxSize() + .padding(Spacings.medium) + ) { + Spacer(modifier = Modifier.height(Spacings.medium)) + MarkdownComponent(markdownElements = uiState.markdownElements) + Spacer( + modifier = Modifier + .height(Spacings.small) + .weight(1f) + ) + SignaturePad( + uiState = uiState, + onAction = onAction, + ) + } +} + +@Preview(uiMode = UI_MODE_NIGHT_YES, showBackground = true) +@Composable +private fun ConsentScreenPreview( + @PreviewParameter(ConsentScreenPreviewProvider::class) uiState: ConsentUiState, +) { + SpeziTheme { + ConsentScreen(uiState = uiState, onAction = { }) + } +} + +@Suppress("MagicNumber") +private class ConsentScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ConsentUiState( + name = PersonNameComponents(givenName = "John", familyName = "Doe"), + paths = mutableListOf(Path().apply { lineTo(100f, 100f) }), + markdownElements = listOf( + MarkdownElement.Heading(1, "Consent"), + MarkdownElement.Paragraph("Please sign below to indicate your consent."), + ), + ), ConsentUiState( + name = PersonNameComponents(givenName = "", familyName = ""), + paths = mutableListOf(), + ) + ) +} + +enum class ConsentScreenTestIdentifier { + ROOT, +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt index d5ca5fa98..c60710650 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt @@ -5,6 +5,7 @@ import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.views.model.ViewState import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDocumentExportConfiguration +import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentViewState internal data class ConsentUiState( val name: PersonNameComponents = PersonNameComponents(), diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt new file mode 100644 index 000000000..79932ec09 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt @@ -0,0 +1,141 @@ +package edu.stanford.spezi.module.onboarding.consent + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.Button +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import edu.stanford.spezi.core.design.theme.Spacings +import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents +import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDocumentExportConfiguration + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun SignaturePad( + uiState: ConsentUiState, + onAction: (ConsentAction) -> Unit, +) { + val keyboardController = LocalSoftwareKeyboardController.current + Column { + OutlinedTextField( + value = uiState.name.givenName ?: "", + onValueChange = { + onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.FIRST_NAME)) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text("First Name") }, + singleLine = true, + trailingIcon = { Icon(Icons.Filled.Info, contentDescription = "Information Icon") } + ) + Spacer(modifier = Modifier.height(Spacings.small)) + OutlinedTextField( + value = uiState.name.familyName ?: "", + onValueChange = { + onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.LAST_NAME)) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Last Name") }, + singleLine = true, + trailingIcon = { + Icon( + Icons.Filled.Info, + contentDescription = "Information Icon" + ) + } + ) + + if ((uiState.name.givenName ?: "").isNotBlank() && (uiState.name.familyName ?: "").isNotBlank()) { + Spacer(modifier = Modifier.height(Spacings.medium)) + Text("Signature:") + SignatureCanvas( + paths = uiState.paths.toMutableList(), + firstName = uiState.name.givenName ?: "", + lastName = uiState.name.familyName ?: "", + onPathAdd = { path -> + onAction(ConsentAction.AddPath(path)) + keyboardController?.hide() + } + ) + Spacer(modifier = Modifier.height(Spacings.medium)) + Row(modifier = Modifier.fillMaxWidth()) { + FilledTonalButton( + onClick = { + if (uiState.paths.isNotEmpty()) { + onAction(ConsentAction.Undo) + } + }, + enabled = uiState.paths.isNotEmpty(), + modifier = Modifier.weight(1f), + ) { + Text("Undo") + } + Spacer(modifier = Modifier.width(Spacings.medium)) + Button( + onClick = { + onAction(ConsentAction.Consent( + documentIdentifier = "consent", + exportConfiguration = ConsentDocumentExportConfiguration() + )) + }, + enabled = uiState.isValidForm, + modifier = Modifier.weight(1f) + ) { + Text("I Consent") + } + } + } + } +} + +@Preview +@Composable +private fun SignaturePadPreview( + @PreviewParameter(SignaturePadPreviewProvider::class) data: SignaturePadPreviewData, +) { + SignaturePad( + uiState = ConsentUiState( + name = PersonNameComponents( + givenName = data.firstName, + familyName = data.lastName, + ), + paths = data.paths + ) + ) {} +} + +private data class SignaturePadPreviewData( + val paths: MutableList, + val firstName: String, + val lastName: String, +) + +private class SignaturePadPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + SignaturePadPreviewData( + paths = mutableListOf(Path()), + firstName = "", + lastName = "" + ), + SignaturePadPreviewData( + paths = mutableListOf(Path().apply { lineTo(100f, 100f) }.apply { lineTo(250f, 200f) }), + firstName = "Jane", + lastName = "Doe" + ) + ) +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocument.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocument.kt index 431f1089c..6a95aa774 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocument.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentDocument.kt @@ -31,7 +31,6 @@ import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.views.model.ViewState import edu.stanford.spezi.module.onboarding.consent.ConsentAction import edu.stanford.spezi.module.onboarding.consent.ConsentUiState -import edu.stanford.spezi.module.onboarding.consent.ConsentViewState import edu.stanford.spezi.module.onboarding.consent.SignatureCanvas import edu.stanford.spezi.module.onboarding.consent.TextFieldType import java.nio.charset.StandardCharsets @@ -157,7 +156,6 @@ private class ConsentDocumentComposablePreviewProvider : PreviewParameterProvide ConsentDocumentComposablePreviewData( paths = mutableListOf(Path().apply { lineTo(100f, 100f) }.apply { lineTo(250f, 200f) }), name = PersonNameComponents(givenName = "Jane", familyName = "Doe") - ) ) } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentViewState.kt similarity index 89% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentViewState.kt index d7520318c..96a363e38 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/ConsentViewState.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.module.onboarding.consent +package edu.stanford.spezi.module.onboarding.spezi.consent import android.graphics.pdf.PdfDocument import edu.stanford.spezi.core.design.views.views.model.ViewState diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/OnboardingConsentComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/OnboardingConsentComposable.kt index ff4ed2729..adbeeb7d5 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/OnboardingConsentComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/spezi/consent/OnboardingConsentComposable.kt @@ -1,6 +1,5 @@ package edu.stanford.spezi.module.onboarding.spezi.consent -import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -16,7 +15,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Path -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel @@ -26,6 +24,7 @@ import edu.stanford.spezi.core.design.component.StringResource.Companion.invoke import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme +import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents import edu.stanford.spezi.core.design.views.views.model.ViewState import edu.stanford.spezi.core.design.views.views.views.text.MarkdownBytes @@ -33,7 +32,6 @@ import edu.stanford.spezi.core.utils.extensions.testIdentifier import edu.stanford.spezi.module.onboarding.consent.ConsentAction import edu.stanford.spezi.module.onboarding.consent.ConsentUiState import edu.stanford.spezi.module.onboarding.consent.ConsentViewModel -import edu.stanford.spezi.module.onboarding.consent.ConsentViewState import edu.stanford.spezi.module.onboarding.core.OnboardingComposable import edu.stanford.spezi.module.onboarding.core.OnboardingTitle import kotlinx.coroutines.launch @@ -73,7 +71,7 @@ internal fun OnboardingConsentComposableContent( val actionScope = rememberCoroutineScope() OnboardingComposable( modifier = Modifier - .testIdentifier(ConsentScreenTestIdentifier.ROOT) + .testIdentifier(OnboardingConsentTestIdentifier.ROOT) .fillMaxSize(), title = { title?.let { @@ -117,10 +115,10 @@ internal fun OnboardingConsentComposableContent( } } -@Preview(uiMode = UI_MODE_NIGHT_YES, showBackground = true) +@ThemePreviews @Composable -private fun ConsentScreenPreview( - @PreviewParameter(ConsentScreenPreviewProvider::class) uiState: ConsentUiState, +private fun OnboardingConsentPreview( + @PreviewParameter(OnboardingConsentPreviewProvider::class) uiState: ConsentUiState, ) { SpeziTheme { OnboardingConsentComposableContent( @@ -136,7 +134,7 @@ private fun ConsentScreenPreview( } @Suppress("MagicNumber") -private class ConsentScreenPreviewProvider : PreviewParameterProvider { +private class OnboardingConsentPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( ConsentUiState( name = PersonNameComponents(givenName = "John", familyName = "Doe"), @@ -152,6 +150,6 @@ private class ConsentScreenPreviewProvider : PreviewParameterProvider Date: Sun, 24 Nov 2024 15:44:45 -0800 Subject: [PATCH 47/51] Remove unused type --- .../spezi/module/onboarding/consent/ConsentUiState.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt index c60710650..8419f293d 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt @@ -17,11 +17,6 @@ internal data class ConsentUiState( (name.givenName?.isNotBlank() ?: false) && (name.familyName?.isNotBlank() ?: false) && paths.isNotEmpty() } -data class FieldState( - val value: String = "", - val error: Boolean = false, -) - enum class TextFieldType { FIRST_NAME, LAST_NAME } From 7759e92575310e6dc4fa4cc92b4bd1625b5957a7 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 25 Nov 2024 16:07:55 -0800 Subject: [PATCH 48/51] Remove consent from ENGAGE-HF --- .../edu/stanford/bdh/engagehf/MainActivity.kt | 4 - .../bdh/engagehf/navigation/Routes.kt | 3 - .../onboarding/EngageConsentManager.kt | 30 ------- .../engagehf/onboarding/OnboardingModule.kt | 6 -- .../onboarding/OnboardingNavigationEvent.kt | 1 - .../onboarding/consent/ConsentManager.kt | 11 --- .../onboarding/consent/ConsentScreen.kt | 89 ------------------- 7 files changed, 144 deletions(-) delete mode 100644 app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt index 15ba15ad1..11b0b803b 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/MainActivity.kt @@ -215,10 +215,6 @@ class MainActivity : FragmentActivity() { Routes.SequentialOnboardingScreen ) - is OnboardingNavigationEvent.ConsentScreen -> navHostController.navigate( - Routes.ConsentScreen - ) - is AppNavigationEvent.AppScreen -> navHostController.navigateTo( route = Routes.AppScreen, clearBackStack = event.clearBackStack diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/Routes.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/Routes.kt index 19ef0c6ca..a8edb111a 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/Routes.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/Routes.kt @@ -28,9 +28,6 @@ sealed class Routes { @Serializable data object OnboardingScreen : Routes() - @Serializable - data object ConsentScreen : Routes() - @Serializable data object ContactScreen : Routes() } diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt deleted file mode 100644 index 784f71d44..000000000 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt +++ /dev/null @@ -1,30 +0,0 @@ -package edu.stanford.bdh.engagehf.onboarding - -import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent -import edu.stanford.spezi.core.navigation.Navigator -import edu.stanford.spezi.core.utils.MessageNotifier -import edu.stanford.spezi.module.onboarding.consent.ConsentManager -import javax.inject.Inject - -class EngageConsentManager @Inject internal constructor( - private val navigator: Navigator, - private val messageNotifier: MessageNotifier, -) : ConsentManager { - - override suspend fun getMarkdownText(): String { - return """ - # Consent - The ENGAGE-HF Android Mobile Application will connect to external devices via Bluetooth to record personal health information, including weight, heart rate, and blood pressure. - - Your personal information will only be shared with the research team conducting the study. - """.trimIndent() - } - - override suspend fun onConsented() { - navigator.navigateTo(AppNavigationEvent.AppScreen(clearBackStack = true)) - } - - override suspend fun onConsentFailure(error: Throwable) { - messageNotifier.notify(message = "Something went wrong, failed to submit the consent!") - } -} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt index a59c013e6..0e3ed0f65 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt @@ -4,7 +4,6 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import edu.stanford.spezi.module.onboarding.consent.ConsentManager import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeRepository import edu.stanford.spezi.module.onboarding.onboarding.OnboardingRepository import edu.stanford.spezi.module.onboarding.sequential.SequentialOnboardingRepository @@ -30,9 +29,4 @@ abstract class OnboardingModule { abstract fun bindSequentialOnboardingRepository( engageSequentialOnboardingRepository: EngageSequentialOnboardingRepository, ): SequentialOnboardingRepository - - @Binds - abstract fun bindOnConsentRepository( - engageConsentManager: EngageConsentManager, - ): ConsentManager } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt index 084bb6644..15756f5a0 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt @@ -7,5 +7,4 @@ sealed class OnboardingNavigationEvent : NavigationEvent { data object InvitationCodeScreen : OnboardingNavigationEvent() data class OnboardingScreen(val clearBackStack: Boolean) : OnboardingNavigationEvent() data object SequentialOnboardingScreen : OnboardingNavigationEvent() - data object ConsentScreen : OnboardingNavigationEvent() } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt deleted file mode 100644 index 784a4ec24..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt +++ /dev/null @@ -1,11 +0,0 @@ -package edu.stanford.spezi.module.onboarding.consent - -/** - * A interface that needs to be implemented and provided by the app to provide the consent text and handle consent actions. - * @see edu.stanford.bdh.engagehf.onboarding.EngageConsentManager - */ -interface ConsentManager { - suspend fun getMarkdownText(): String - suspend fun onConsented() - suspend fun onConsentFailure(error: Throwable) -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt deleted file mode 100644 index 0fa4a960f..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -package edu.stanford.spezi.module.onboarding.consent - -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.hilt.navigation.compose.hiltViewModel -import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent -import edu.stanford.spezi.core.design.component.markdown.MarkdownElement -import edu.stanford.spezi.core.design.theme.Spacings -import edu.stanford.spezi.core.design.theme.SpeziTheme -import edu.stanford.spezi.core.design.views.personalinfo.PersonNameComponents -import edu.stanford.spezi.core.utils.extensions.testIdentifier - -@Composable -fun ConsentScreen() { - val viewModel: ConsentViewModel = hiltViewModel() - val uiState by viewModel.uiState.collectAsState() - - ConsentScreen( - onAction = viewModel::onAction, uiState = uiState - ) -} - -@Composable -private fun ConsentScreen( - uiState: ConsentUiState, - onAction: (ConsentAction) -> Unit, -) { - Column( - modifier = Modifier - .testIdentifier(ConsentScreenTestIdentifier.ROOT) - .fillMaxSize() - .padding(Spacings.medium) - ) { - Spacer(modifier = Modifier.height(Spacings.medium)) - MarkdownComponent(markdownElements = uiState.markdownElements) - Spacer( - modifier = Modifier - .height(Spacings.small) - .weight(1f) - ) - SignaturePad( - uiState = uiState, - onAction = onAction, - ) - } -} - -@Preview(uiMode = UI_MODE_NIGHT_YES, showBackground = true) -@Composable -private fun ConsentScreenPreview( - @PreviewParameter(ConsentScreenPreviewProvider::class) uiState: ConsentUiState, -) { - SpeziTheme { - ConsentScreen(uiState = uiState, onAction = { }) - } -} - -@Suppress("MagicNumber") -private class ConsentScreenPreviewProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - ConsentUiState( - name = PersonNameComponents(givenName = "John", familyName = "Doe"), - paths = mutableListOf(Path().apply { lineTo(100f, 100f) }), - markdownElements = listOf( - MarkdownElement.Heading(1, "Consent"), - MarkdownElement.Paragraph("Please sign below to indicate your consent."), - ), - ), ConsentUiState( - name = PersonNameComponents(givenName = "", familyName = ""), - paths = mutableListOf(), - ) - ) -} - -enum class ConsentScreenTestIdentifier { - ROOT, -} From bc9663cafc22268a703fa9cd49dc4b8350187f00 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 25 Nov 2024 16:20:36 -0800 Subject: [PATCH 49/51] Remove tests --- .../onboarding/EngageConsentManagerTest.kt | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 app/src/test/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManagerTest.kt diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManagerTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManagerTest.kt deleted file mode 100644 index bd2e8474a..000000000 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManagerTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package edu.stanford.bdh.engagehf.onboarding - -import com.google.common.truth.Truth.assertThat -import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent -import edu.stanford.spezi.core.navigation.Navigator -import edu.stanford.spezi.core.testing.runTestUnconfined -import edu.stanford.spezi.core.utils.MessageNotifier -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.verify -import org.junit.Before -import org.junit.Test - -class EngageConsentManagerTest { - private val navigator: Navigator = mockk() - private val messageNotifier: MessageNotifier = mockk() - private val manager = EngageConsentManager( - navigator = navigator, - messageNotifier = messageNotifier, - ) - - @Before - fun setup() { - every { navigator.navigateTo(AppNavigationEvent.AppScreen(true)) } just Runs - every { messageNotifier.notify(message = any(), any()) } just Runs - } - - @Test - fun `it should return the correct markdown test`() = runTestUnconfined { - // given - val expectedText = """ - # Consent - The ENGAGE-HF Android Mobile Application will connect to external devices via Bluetooth to record personal health information, including weight, heart rate, and blood pressure. - - Your personal information will only be shared with the research team conducting the study. - """.trimIndent() - - // when - val result = manager.getMarkdownText() - - // then - assertThat(result).isEqualTo(expectedText) - } - - @Test - fun `it should navigate to bluetooth screen on consented`() = runTestUnconfined { - // given - val navigationEvent = AppNavigationEvent.AppScreen(clearBackStack = true) - - // when - manager.onConsented() - - // then - verify { navigator.navigateTo(event = navigationEvent) } - } - - @Test - fun `it should notify error message on on consent failure`() = runTestUnconfined { - // given - val message = "Something went wrong, failed to submit the consent!" - - // when - manager.onConsentFailure(error = mockk()) - - // then - verify { messageNotifier.notify(message = message) } - } -} From 2c4e8a73c17b14708c08540690f06eacd01b3710 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 25 Nov 2024 17:34:00 -0800 Subject: [PATCH 50/51] update tests --- .../consent/ConsentViewModelTest.kt | 68 +++++++------------ 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/modules/onboarding/src/test/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModelTest.kt b/modules/onboarding/src/test/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModelTest.kt index b079ba286..4893a143f 100644 --- a/modules/onboarding/src/test/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModelTest.kt +++ b/modules/onboarding/src/test/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModelTest.kt @@ -1,12 +1,14 @@ package edu.stanford.spezi.module.onboarding.consent +import android.graphics.pdf.PdfDocument import androidx.compose.ui.graphics.Path import com.google.common.truth.Truth.assertThat -import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.component.markdown.MarkdownParser import edu.stanford.spezi.core.testing.CoroutineTestRule import edu.stanford.spezi.core.testing.runTestUnconfined import edu.stanford.spezi.module.account.manager.UserSessionManager +import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDataSource +import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDocumentExportConfiguration import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -21,16 +23,14 @@ class ConsentViewModelTest { @get:Rule val coroutineTestRule = CoroutineTestRule() - private val consentManager: ConsentManager = mockk(relaxed = true) private val markdownParser: MarkdownParser = mockk(relaxed = true) - private val pdfCreationService: PdfCreationService = mockk(relaxed = true) + private val consentDataSource: ConsentDataSource = mockk(relaxed = true) + private val consentPdfService: ConsentPdfService = mockk(relaxed = true) private val userSessionManager: UserSessionManager = mockk(relaxed = true) private val viewModel by lazy { ConsentViewModel( - consentManager = consentManager, - markdownParser = markdownParser, - pdfCreationService = pdfCreationService, - userSessionManager = userSessionManager + pdfService = consentPdfService, + consentDataSource = consentDataSource, ) } @@ -50,7 +50,7 @@ class ConsentViewModelTest { // Then val uiState = viewModel.uiState.first() - assertThat(name).isEqualTo(uiState.firstName.value) + assertThat(name).isEqualTo(uiState.name.givenName) } @Test @@ -65,7 +65,7 @@ class ConsentViewModelTest { // Then val uiState = viewModel.uiState.first() - assertThat(lastName).isEqualTo(uiState.lastName.value) + assertThat(lastName).isEqualTo(uiState.name.familyName) } @Test @@ -95,47 +95,27 @@ class ConsentViewModelTest { assertThat(uiState.paths.size).isEqualTo(0) } - @Test - fun `init block should fetch ConsentData correctly`() = runTestUnconfined { - // Given - val markdownText = "some markdown text" - val elements: List = emptyList() - every { markdownParser.parse(markdownText) } returns elements - coEvery { consentManager.getMarkdownText() } returns markdownText - - // When - val uiState = viewModel.uiState.first() - - // Then - assertThat(uiState.markdownElements).isEqualTo(elements) - } - + /* @Test fun `it should invoke handle consent action correctly on success case`() = runTestUnconfined { // given - val pdfBytes = byteArrayOf() - coEvery { pdfCreationService.createPdf(viewModel.uiState.value) } returns pdfBytes - coEvery { userSessionManager.uploadConsentPdf(pdfBytes) } returns Result.success(Unit) - - // when - viewModel.onAction(action = ConsentAction.Consent) - - // then - coVerify { consentManager.onConsented() } - } - - @Test - fun `it should invoke handle consent action correctly on error case`() = runTestUnconfined { - // given - val error: Throwable = mockk() - val pdfBytes = byteArrayOf() - coEvery { pdfCreationService.createPdf(viewModel.uiState.value) } returns pdfBytes - coEvery { userSessionManager.uploadConsentPdf(pdfBytes) } returns Result.failure(error) + val pdfDocument = PdfDocument() + val documentIdentifier = "testDocument" + val configuration = ConsentDocumentExportConfiguration() + coEvery { + consentPdfService.createDocument( + configuration, + viewModel.uiState.value.name, + viewModel.uiState.value.paths, + viewModel.uiState.value.markdownElements + ) + } returns pdfDocument // when - viewModel.onAction(action = ConsentAction.Consent) + viewModel.onAction(action = ConsentAction.Consent(documentIdentifier, configuration)) // then - coVerify { consentManager.onConsentFailure(error) } + coVerify { consentDataSource.store({ pdfDocument }, documentIdentifier) } } + */ } From 32746ae6f507e230f04d0284c30b15de475ae3f6 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Tue, 26 Nov 2024 09:35:20 -0800 Subject: [PATCH 51/51] detekt --- .../spezi/module/onboarding/consent/ConsentViewModelTest.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/onboarding/src/test/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModelTest.kt b/modules/onboarding/src/test/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModelTest.kt index 4893a143f..d35e2c81c 100644 --- a/modules/onboarding/src/test/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModelTest.kt +++ b/modules/onboarding/src/test/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModelTest.kt @@ -1,6 +1,5 @@ package edu.stanford.spezi.module.onboarding.consent -import android.graphics.pdf.PdfDocument import androidx.compose.ui.graphics.Path import com.google.common.truth.Truth.assertThat import edu.stanford.spezi.core.design.component.markdown.MarkdownParser @@ -8,9 +7,6 @@ import edu.stanford.spezi.core.testing.CoroutineTestRule import edu.stanford.spezi.core.testing.runTestUnconfined import edu.stanford.spezi.module.account.manager.UserSessionManager import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDataSource -import edu.stanford.spezi.module.onboarding.spezi.consent.ConsentDocumentExportConfiguration -import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.first