diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index ff5f39eed..0bf0c5417 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -42,6 +42,7 @@ import org.hisp.dhis.common.screens.InputCoordinateScreen import org.hisp.dhis.common.screens.InputDateTimeScreen import org.hisp.dhis.common.screens.InputDropDownScreen import org.hisp.dhis.common.screens.InputEmailScreen +import org.hisp.dhis.common.screens.InputFileResourceScreen import org.hisp.dhis.common.screens.InputIntegerScreen import org.hisp.dhis.common.screens.InputLetterScreen import org.hisp.dhis.common.screens.InputLinkScreen @@ -87,7 +88,7 @@ fun App() { @Composable fun Main() { - val currentScreen = remember { mutableStateOf(Components.INPUT_BARCODE) } + val currentScreen = remember { mutableStateOf(Components.INPUT_FILE_RESOURCE) } var expanded by remember { mutableStateOf(false) } Column( @@ -182,6 +183,7 @@ fun Main() { Components.INPUT_POLYGON -> InputPolygonScreen() Components.INPUT_ORG_UNIT -> InputOrgUnitScreen() Components.IMAGE_BLOCK -> ImageBlockScreen() + Components.INPUT_FILE_RESOURCE -> InputFileResourceScreen() Components.INPUT_DROPDOWN -> InputDropDownScreen() Components.INPUT_DATE_TIME -> InputDateTimeScreen() Components.INPUT_COORDINATE -> InputCoordinateScreen() diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt index ca09185c7..8d3397346 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt @@ -49,6 +49,7 @@ enum class Components(val label: String) { INPUT_POLYGON("Input Polygon"), INPUT_ORG_UNIT("Input Org. Unit"), IMAGE_BLOCK("Image Block"), + INPUT_FILE_RESOURCE("Input File Resource"), INPUT_DROPDOWN("Input Dropdown"), INPUT_DATE_TIME("Input Date Time"), INPUT_COORDINATE("Input Coordinate"), diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputFileResourceScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputFileResourceScreen.kt new file mode 100644 index 000000000..018ad82fb --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputFileResourceScreen.kt @@ -0,0 +1,82 @@ +package org.hisp.dhis.common.screens + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.InputFileResource +import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState +import org.hisp.dhis.mobile.ui.designsystem.component.UploadFileState +import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource + +@Composable +fun InputFileResourceScreen() { + ColumnComponentContainer( + title = "Input File Component", + ) { + val currentFileName: MutableState = + mutableStateOf("filename.extension") + val currentFileWeight: MutableState = + mutableStateOf("524kb") + val currentFileName2: MutableState = + mutableStateOf("filename.extension") + val currentFileWeight2: MutableState = + mutableStateOf("524kb") + + InputFileResource( + title = "Label", + buttonText = provideStringResource("add_file"), + fileName = currentFileName, + fileWeight = currentFileWeight, + onSelectFile = { + currentFileName.value = "file" + currentFileWeight.value = "weight" + }, + onUploadFile = {}, + ) + InputFileResource( + title = "Label", + buttonText = provideStringResource("add_file"), + fileName = currentFileName, + fileWeight = currentFileWeight, + uploadFileState = UploadFileState.UPLOADING, + inputShellState = InputShellState.FOCUSED, + onSelectFile = {}, + onUploadFile = {}, + ) + InputFileResource( + title = "Label", + buttonText = provideStringResource("add_file"), + fileName = currentFileName2, + fileWeight = currentFileWeight2, + uploadFileState = UploadFileState.LOADED, + onSelectFile = {}, + onUploadFile = {}, + ) + InputFileResource( + title = "Label", + buttonText = provideStringResource("add_file"), + fileName = currentFileName, + fileWeight = currentFileWeight, + inputShellState = InputShellState.DISABLED, + onSelectFile = { + currentFileName.value = "file" + currentFileWeight.value = "weight" + }, + onUploadFile = {}, + ) + InputFileResource( + title = "Label", + buttonText = provideStringResource("add_file"), + fileName = currentFileName, + fileWeight = currentFileWeight, + inputShellState = InputShellState.DISABLED, + uploadFileState = UploadFileState.LOADED, + onSelectFile = { + currentFileName.value = "file" + currentFileWeight.value = "weight" + }, + onUploadFile = {}, + ) + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputFileResource.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputFileResource.kt new file mode 100644 index 000000000..e124efed8 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputFileResource.kt @@ -0,0 +1,186 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.FileDownload +import androidx.compose.material.icons.outlined.FileUpload +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import org.hisp.dhis.mobile.ui.designsystem.component.UploadFileState.ADD +import org.hisp.dhis.mobile.ui.designsystem.component.UploadFileState.LOADED +import org.hisp.dhis.mobile.ui.designsystem.component.UploadFileState.UPLOADING +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +const val INPUT_FILE_TEST_TAG = "INPUT_FILE_RESOURCE_" +const val CLEAR_BUTTON_TEST_TAG = "CLEAR_BUTTON" +const val UPLOAD_BUTTON_TEST_TAG = "UPLOAD_BUTTON" +const val ADD_BUTTON_TEST_TAG = "ADD_BUTTON" +const val PROGRESS_INDICATOR_TEST_TAG = "PROGRESS_INDICATOR" +const val UPLOAD_TEXT_FILE_NAME_TEST_TAG = "UPLOAD_TEXT_FILE_NAME" +const val UPLOAD_TEXT_FILE_WEIGHT_TEST_TAG = "UPLOAD_TEXT_FILE_WEIGHT" +const val SUPPORTING_TEXT_TEST_TAG = "SUPPORTING_TEXT" + +@Composable +fun InputFileResource( + title: String, + buttonText: String, + fileName: MutableState = mutableStateOf(null), + fileWeight: MutableState = mutableStateOf(null), + onSelectFile: () -> Unit, + onUploadFile: () -> Unit, + onClear: () -> Unit = {}, + uploadFileState: UploadFileState = ADD, + inputShellState: InputShellState = InputShellState.UNFOCUSED, + supportingText: List? = null, + modifier: Modifier = Modifier, +) { + var currentState by remember { + mutableStateOf(uploadFileState) + } + + val primaryButton: @Composable (() -> Unit)? = if (currentState == LOADED && inputShellState != InputShellState.DISABLED) { + { + IconButton( + modifier = Modifier.testTag(INPUT_FILE_TEST_TAG + CLEAR_BUTTON_TEST_TAG), + icon = { + Icon( + imageVector = Icons.Outlined.Cancel, + contentDescription = "Icon Button", + ) + }, + onClick = { + currentState = ADD + onClear.invoke() + }, + ) + } + } else { + null + } + + val secondaryButton: @Composable (() -> Unit)? = + if (currentState == LOADED) { + { + SquareIconButton( + modifier = Modifier.testTag(INPUT_FILE_TEST_TAG + UPLOAD_BUTTON_TEST_TAG), + icon = { + Icon( + imageVector = Icons.Outlined.FileDownload, + contentDescription = "Download Icon Button", + ) + }, + ) { + currentState = UPLOADING + onUploadFile.invoke() + } + } + } else { + null + } + + InputShell( + title, + state = inputShellState, + supportingText = { + supportingText?.forEach { label -> + SupportingText( + label.text, + label.state, + modifier = modifier.testTag(INPUT_FILE_TEST_TAG + SUPPORTING_TEXT_TEST_TAG), + ) + } + }, + inputField = { + when (currentState) { + ADD -> { + ButtonBlock( + primaryButton = { + Button( + enabled = inputShellState != InputShellState.DISABLED, + modifier = Modifier + .testTag(INPUT_FILE_TEST_TAG + ADD_BUTTON_TEST_TAG) + .padding(end = Spacing.Spacing16) + .fillMaxWidth(), + style = ButtonStyle.KEYBOARDKEY, + text = buttonText, + icon = { + Icon( + imageVector = Icons.Outlined.FileUpload, + contentDescription = "Upload Icon Button", + ) + }, + ) { + currentState = LOADED + onSelectFile.invoke() + } + }, + ) + } + UPLOADING -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Box( + Modifier + .padding(top = Spacing.Spacing8, bottom = Spacing.Spacing8) + .size(Spacing.Spacing48), + ) { + ProgressIndicator( + modifier = Modifier.testTag(INPUT_FILE_TEST_TAG + PROGRESS_INDICATOR_TEST_TAG), + type = ProgressIndicatorType.CIRCULAR, + ) + } + } + } + LOADED -> { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + fileName.value?.let { + Text( + text = it, + color = if (inputShellState != InputShellState.DISABLED) TextColor.OnSurface else TextColor.OnDisabledSurface, + maxLines = 1, + modifier = Modifier.testTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_NAME_TEST_TAG), + ) + } + fileWeight.value?.let { + Text( + text = " $it", + color = TextColor.OnDisabledSurface, + maxLines = 1, + modifier = Modifier.testTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_WEIGHT_TEST_TAG), + ) + } + } + } + } + }, + primaryButton = primaryButton, + secondaryButton = secondaryButton, + modifier = modifier, + ) +} + +enum class UploadFileState { + ADD, + UPLOADING, + LOADED, +} diff --git a/designsystem/src/commonMain/resources/values/strings_en.xml b/designsystem/src/commonMain/resources/values/strings_en.xml index 6cab9e0be..9c6c192bf 100644 --- a/designsystem/src/commonMain/resources/values/strings_en.xml +++ b/designsystem/src/commonMain/resources/values/strings_en.xml @@ -15,6 +15,7 @@ Show fields Hide fields Next + Add file Yes No Enter phone number diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputFileResourceTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputFileResourceTest.kt new file mode 100644 index 000000000..b073af65b --- /dev/null +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputFileResourceTest.kt @@ -0,0 +1,127 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource +import org.junit.Rule +import org.junit.Test + +class InputFileResourceTest { + + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldShowLoaderAfterUploadFile() { + rule.setContent { + InputFileResource( + title = "Label", + buttonText = provideStringResource("add_file"), + fileName = mutableStateOf("filename.extension"), + fileWeight = mutableStateOf("524kb"), + uploadFileState = UploadFileState.LOADED, + onSelectFile = {}, + onUploadFile = {}, + ) + } + + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_BUTTON_TEST_TAG).performClick() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + PROGRESS_INDICATOR_TEST_TAG).assertExists() + } + + @Test + fun shouldShowClearButtonAndHelperWhenFileIsSelected() { + val testFileName: MutableState = mutableStateOf("filename.extension") + val testFileWeight: MutableState = mutableStateOf("524kb") + + rule.setContent { + InputFileResource( + title = "Label", + buttonText = provideStringResource("add_file"), + fileName = testFileName, + fileWeight = testFileWeight, + onSelectFile = {}, + onUploadFile = {}, + ) + } + + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + ADD_BUTTON_TEST_TAG).performClick() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_BUTTON_TEST_TAG).assertExists() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_NAME_TEST_TAG).assertExists() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_WEIGHT_TEST_TAG).assertExists() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_NAME_TEST_TAG).assert(hasText(testFileName.value.toString())) + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_WEIGHT_TEST_TAG).assert(hasText(" " + testFileWeight.value.toString())) + } + + @Test + fun shouldDisplayButtonWithCustomText() { + rule.setContent { + InputFileResource( + title = "Label", + buttonText = "select a file", + fileName = mutableStateOf("filename.extension"), + fileWeight = mutableStateOf("524kb"), + onSelectFile = {}, + onUploadFile = {}, + ) + } + + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + ADD_BUTTON_TEST_TAG).assertExists() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + ADD_BUTTON_TEST_TAG).assert(hasText("select a file")) + } + + @Test + fun shouldChangeFileNameAndFileWeightAfterModifyIt() { + val testFileName: MutableState = mutableStateOf("test.filename.extension") + val testFileWeight: MutableState = mutableStateOf("256kb") + var newFileName: String? = null + var newFileWeight: String? = null + + rule.setContent { + InputFileResource( + title = "Label", + buttonText = provideStringResource("add_file"), + fileName = testFileName, + fileWeight = testFileWeight, + uploadFileState = UploadFileState.LOADED, + onSelectFile = { + testFileName.value = newFileName + testFileWeight.value = newFileWeight + }, + onUploadFile = {}, + ) + } + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_NAME_TEST_TAG).assert(hasText("test.filename.extension")) + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_WEIGHT_TEST_TAG).assert(hasText(" 256kb")) + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + CLEAR_BUTTON_TEST_TAG).performClick() + newFileName = "test_file" + newFileWeight = "512gb" + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + ADD_BUTTON_TEST_TAG).performClick() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_NAME_TEST_TAG).assert(hasText("test_file")) + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + UPLOAD_TEXT_FILE_WEIGHT_TEST_TAG).assert(hasText(" 512gb")) + } + + @Test + fun shouldAppearIconTextButtonWhenUploadIsCancelled() { + rule.setContent { + InputFileResource( + title = "Label", + buttonText = "add file", + fileName = mutableStateOf("filename.extension"), + fileWeight = mutableStateOf("524kb"), + uploadFileState = UploadFileState.LOADED, + onSelectFile = {}, + onUploadFile = {}, + ) + } + + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + CLEAR_BUTTON_TEST_TAG).performClick() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + ADD_BUTTON_TEST_TAG).assertExists() + rule.onNodeWithTag(INPUT_FILE_TEST_TAG + ADD_BUTTON_TEST_TAG).assert(hasText("add file")) + } +}