diff --git a/app/src/main/java/com/anytypeio/anytype/ui/primitives/EditTypePropertiesFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/EditTypePropertiesFragment.kt index 2eae4f407d..f72a23b72d 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/primitives/EditTypePropertiesFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/EditTypePropertiesFragment.kt @@ -2,20 +2,33 @@ package com.anytypeio.anytype.ui.primitives import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import androidx.fragment.compose.content +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.anytypeio.anytype.R import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.core_ui.views.BaseAlertDialog import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior +import com.anytypeio.anytype.core_utils.ext.subscribe import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModel import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModelFactory +import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModel +import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModel.EditTypePropertiesCommand import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams +import com.anytypeio.anytype.feature_properties.add.UiEditTypePropertiesErrorState +import com.anytypeio.anytype.feature_properties.add.ui.AddFieldScreen import javax.inject.Inject class EditTypePropertiesFragment : BaseBottomSheetComposeFragment() { @@ -33,6 +46,75 @@ class EditTypePropertiesFragment : BaseBottomSheetComposeFragment() { savedInstanceState: Bundle? ) = content { MaterialTheme { + AddFieldScreen( + state = vm.uiState.collectAsStateWithLifecycle().value, + uiStateEditProperty = vm.uiPropertyEditState.collectAsStateWithLifecycle().value, + event = vm::onEvent + ) + ErrorScreen() + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun ErrorScreen() { + val errorStateScreen = vm.errorState.collectAsStateWithLifecycle().value + if (errorStateScreen is UiEditTypePropertiesErrorState.Show) { + when (val r = errorStateScreen.reason) { + is UiEditTypePropertiesErrorState.Reason.ErrorAddingProperty -> { + BaseAlertDialog( + dialogText = stringResource(id = R.string.add_property_error_add), + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + + is UiEditTypePropertiesErrorState.Reason.ErrorCreatingProperty -> { + BaseAlertDialog( + dialogText = stringResource(id = R.string.add_property_error_create_new), + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + + is UiEditTypePropertiesErrorState.Reason.ErrorUpdatingProperty -> { + BaseAlertDialog( + dialogText = stringResource(id = R.string.add_property_error_update), + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + + is UiEditTypePropertiesErrorState.Reason.Other -> { + BaseAlertDialog( + dialogText = r.msg, + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupBottomSheetBehavior(DEFAULT_PADDING_TOP) + } + + override fun onStart() { + super.onStart() + jobs += lifecycleScope.subscribe(vm.commands) { command -> execute(command) } + } + + private fun execute(command: EditTypePropertiesCommand) { + when (command) { + is EditTypePropertiesCommand.Exit -> { + findNavController().popBackStack() + } } } diff --git a/feature-properties/src/main/java/com/anytypeio/anytype/feature_properties/EditTypePropertiesViewModel.kt b/feature-properties/src/main/java/com/anytypeio/anytype/feature_properties/EditTypePropertiesViewModel.kt index 0905994866..a110ff9a66 100644 --- a/feature-properties/src/main/java/com/anytypeio/anytype/feature_properties/EditTypePropertiesViewModel.kt +++ b/feature-properties/src/main/java/com/anytypeio/anytype/feature_properties/EditTypePropertiesViewModel.kt @@ -1,13 +1,41 @@ package com.anytypeio.anytype.feature_properties import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_ui.extensions.simpleIcon +import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.`object`.SetObjectDetails import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields import com.anytypeio.anytype.domain.relations.CreateRelation import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.feature_properties.add.UiEditTypePropertiesEvent import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams +import com.anytypeio.anytype.feature_properties.add.UiEditTypePropertiesErrorState +import com.anytypeio.anytype.feature_properties.add.UiEditTypePropertiesItem +import com.anytypeio.anytype.feature_properties.add.UiEditTypePropertiesState +import com.anytypeio.anytype.feature_properties.add.mapToStateItem +import com.anytypeio.anytype.feature_properties.edit.UiEditPropertyState +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import timber.log.Timber class EditTypePropertiesViewModel( private val vmParams: EditTypePropertiesVmParams, @@ -18,5 +46,296 @@ class EditTypePropertiesViewModel( private val setObjectDetails: SetObjectDetails, private val setObjectTypeRecommendedFields: SetObjectTypeRecommendedFields ) : ViewModel() { + + private val _uiState = MutableStateFlow(UiEditTypePropertiesState.Companion.EMPTY) + val uiState = _uiState.asStateFlow() + + private val _errorState = + MutableStateFlow(UiEditTypePropertiesErrorState.Hidden) + val errorState = _errorState.asStateFlow() + + val uiPropertyEditState = + MutableStateFlow(UiEditPropertyState.Hidden) + + private val _commands = MutableSharedFlow() + val commands = _commands.asSharedFlow() + + private val input = MutableStateFlow("") + @OptIn(FlowPreview::class) + private val query = input.take(1).onCompletion { + emitAll( + input.drop(1).debounce(DEBOUNCE_DURATION).distinctUntilChanged() + ) + } + + //region Init + init { + setupAddNewPropertiesState() + } + + private fun setupAddNewPropertiesState() { + viewModelScope.launch { + combine( + storeOfObjectTypes.trackChanges(), + storeOfRelations.trackChanges(), + query + ) { _, _, queryText -> + val objType = storeOfObjectTypes.get(id = vmParams.objectTypeId) + if (objType != null) { + val typeKeys = + objType.recommendedRelations + objType.recommendedFeaturedRelations + objType.recommendedFileRelations + objType.recommendedHiddenRelations + queryText to filterProperties( + allProperties = storeOfRelations.getAll(), + typeKeys = typeKeys, + queryText = queryText + ) + } else { + Timber.w("Object type:[${vmParams.objectTypeId}] not found in the store") + queryText to emptyList() + } + }.catch { + Timber.e(it, "Error while filtering properties") + _errorState.value = UiEditTypePropertiesErrorState.Show( + UiEditTypePropertiesErrorState.Reason.Other("Error while filtering properties") + ) + } + .collect { (queryText, filteredProperties) -> + + val sortedExistingItems = filteredProperties.mapNotNull { field -> + field.mapToStateItem( + stringResourceProvider = stringResourceProvider + ) + }.sortedBy { it.title } + + setUiState( + queryText = queryText, + sortedExistingProperties = sortedExistingItems + ) + } + } + } + //endregion + + //region Ui State + private fun setUiState( + queryText: String, + sortedExistingProperties: List + ) { + val items = buildList { + if (queryText.isNotEmpty()) { + add(UiEditTypePropertiesItem.Create(title = queryText)) + val propertiesFormatItems = filterPropertiesFormats(queryText) + if (propertiesFormatItems.isNotEmpty()) { + add(UiEditTypePropertiesItem.Section.Types()) + addAll(propertiesFormatItems) + } + if (sortedExistingProperties.isNotEmpty()) { + add(UiEditTypePropertiesItem.Section.Existing()) + addAll(sortedExistingProperties) + } + } else { + val propertiesFormatItems = filterPropertiesFormats(queryText) + if (propertiesFormatItems.isNotEmpty()) { + add(UiEditTypePropertiesItem.Section.Types()) + addAll(propertiesFormatItems) + } + if (sortedExistingProperties.isNotEmpty()) { + add(UiEditTypePropertiesItem.Section.Existing()) + addAll(sortedExistingProperties) + } + } + } + + _uiState.value = UiEditTypePropertiesState(items = items) + } + + private fun filterPropertiesFormats(query: String): List { + return if (query.isNotEmpty()) { + UiEditTypePropertiesState.Companion.PROPERTIES_FORMATS.map { format -> + UiEditTypePropertiesItem.Format( + format = format, + prettyName = stringResourceProvider.getPropertiesFormatPrettyString(format) + ) + }.filter { it.prettyName.contains(query, ignoreCase = true) } + } else { + UiEditTypePropertiesState.Companion.PROPERTIES_FORMATS.map { format -> + UiEditTypePropertiesItem.Format( + format = format, + prettyName = stringResourceProvider.getPropertiesFormatPrettyString(format) + ) + } + } + } + + private fun filterProperties( + allProperties: List, + typeKeys: List, + queryText: String + ): List = allProperties.filter { field -> + field.key !in typeKeys && + field.isValidToUse && + (queryText.isBlank() || field.name?.contains(queryText, ignoreCase = true) == true) + } + + fun hideError() { + _errorState.value = UiEditTypePropertiesErrorState.Hidden + } + //endregion + + //region Ui Events + fun onEvent(event: UiEditTypePropertiesEvent) { + when (event) { + is UiEditTypePropertiesEvent.OnCreate -> { + val format = event.item.format + uiPropertyEditState.value = UiEditPropertyState.Visible.New( + name = event.item.title, + formatName = stringResourceProvider.getPropertiesFormatPrettyString(format), + formatIcon = format.simpleIcon(), + format = format, + ) + } + + is UiEditTypePropertiesEvent.OnExistingClicked -> { + viewModelScope.launch { + val objType = storeOfObjectTypes.get(vmParams.objectTypeId) + if (objType != null) { + proceedWithSetRecommendedProperties( + properties = objType.recommendedRelations + event.item.id + ) + } + } + } + + is UiEditTypePropertiesEvent.OnSearchQueryChanged -> { + input.value = event.query + } + + is UiEditTypePropertiesEvent.OnTypeClicked -> { + val format = event.item.format + uiPropertyEditState.value = UiEditPropertyState.Visible.New( + name = "", + formatName = stringResourceProvider.getPropertiesFormatPrettyString(format), + formatIcon = format.simpleIcon(), + format = format, + ) + } + + UiEditTypePropertiesEvent.OnEditPropertyScreenDismissed -> { + uiPropertyEditState.value = UiEditPropertyState.Hidden + } + + UiEditTypePropertiesEvent.OnCreateNewButtonClicked -> { + proceedWithCreatingRelation() + } + + UiEditTypePropertiesEvent.OnSaveButtonClicked -> { + proceedWithUpdatingRelation() + } + + is UiEditTypePropertiesEvent.OnPropertyNameUpdate -> { + val state = uiPropertyEditState.value as? UiEditPropertyState.Visible ?: return + uiPropertyEditState.value = when (state) { + is UiEditPropertyState.Visible.Edit -> state.copy(name = event.name) + is UiEditPropertyState.Visible.New -> state.copy(name = event.name) + is UiEditPropertyState.Visible.View -> state + } + } + } + } + //endregion + + //region Use Cases + private fun proceedWithUpdatingRelation() { + val state = uiPropertyEditState.value as? UiEditPropertyState.Visible.Edit ?: return + viewModelScope.launch { + val params = SetObjectDetails.Params( + ctx = state.id, + details = mapOf( + Relations.NAME to state.name, + Relations.RELATION_FORMAT to state.format + ) + ) + setObjectDetails.async(params).fold( + onSuccess = { + Timber.d("Relation updated: $it") + }, + onFailure = { error -> + Timber.e(error, "Failed to update relation") + _errorState.value = UiEditTypePropertiesErrorState.Show( + UiEditTypePropertiesErrorState.Reason.ErrorUpdatingProperty(error.message ?: "") + ) + } + ) + } + } + + private fun proceedWithCreatingRelation() { + viewModelScope.launch { + val state = uiPropertyEditState.value as? UiEditPropertyState.Visible ?: return@launch + val (name, format) = when (state) { + is UiEditPropertyState.Visible.Edit -> state.name to state.format + is UiEditPropertyState.Visible.View -> state.name to state.format + is UiEditPropertyState.Visible.New -> state.name to state.format + } + val params = CreateRelation.Params( + space = vmParams.spaceId.id, + format = format, + name = name, + limitObjectTypes = emptyList(), + prefilled = emptyMap() + ) + createRelation(params).process( + success = { relation -> + Timber.d("Relation created: $relation") + val objType = storeOfObjectTypes.get(vmParams.objectTypeId) + if (objType != null) { + proceedWithSetRecommendedProperties( + properties = objType.recommendedRelations + listOf(relation.id) + ) + } + uiPropertyEditState.value = UiEditPropertyState.Hidden + _commands.emit(EditTypePropertiesCommand.Exit) + }, + failure = { error -> + Timber.e(error, "Failed to create relation") + _errorState.value = UiEditTypePropertiesErrorState.Show( + UiEditTypePropertiesErrorState.Reason.ErrorCreatingProperty(error.message ?: "") + ) + } + ) + } + } + + private fun proceedWithSetRecommendedProperties(properties: List) { + val params = SetObjectTypeRecommendedFields.Params( + objectTypeId = vmParams.objectTypeId, + fields = properties + ) + viewModelScope.launch { + setObjectTypeRecommendedFields.async(params).fold( + onSuccess = { + Timber.d("Recommended fields set") + _commands.emit(EditTypePropertiesCommand.Exit) + }, + onFailure = { error -> + Timber.e(error, "Error while setting recommended fields") + _errorState.value = UiEditTypePropertiesErrorState.Show( + UiEditTypePropertiesErrorState.Reason.ErrorAddingProperty(error.message ?: "") + ) + } + ) + } + } + //endregion + + //region Commands + sealed class EditTypePropertiesCommand { + data object Exit : EditTypePropertiesCommand() + } + //endregion + + companion object { + private const val DEBOUNCE_DURATION = 300L + } }