From d4d1b3ea3ea91925401c1835bfddc13757e77474 Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:56:38 +0100 Subject: [PATCH] DROID-2121 Relations | Tags & status screen logic (#817) --- .../anytype/di/feature/TagStatusDI.kt | 6 +- .../anytype/ui/editor/EditorFragment.kt | 1 + .../ui/relations/value/TagStatusFragment.kt | 45 ++- .../anytype/core_ui/relations/Common.kt | 10 +- .../anytype/core_ui/relations/ItemMenu.kt | 75 ++++ .../core_ui/relations/RelationsLazyList.kt | 42 +-- .../anytype/core_ui/relations/StatusItem.kt | 48 ++- .../anytype/core_ui/relations/TagItem.kt | 64 ++-- .../domain/objects/options/GetOptions.kt | 4 +- .../relations/model/RelationsListItem.kt | 30 -- .../value/tagstatus/TagStatusViewModel.kt | 346 +++++++++++++++++- .../tagstatus/TagStatusViewModelFactory.kt | 6 +- 12 files changed, 564 insertions(+), 113 deletions(-) create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/ItemMenu.kt delete mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/model/RelationsListItem.kt diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/TagStatusDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/TagStatusDI.kt index e1cc711f5..4eed7f731 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/TagStatusDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/TagStatusDI.kt @@ -8,7 +8,7 @@ import com.anytypeio.anytype.domain.`object`.UpdateDetail import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.options.GetOptions import com.anytypeio.anytype.domain.workspace.SpaceManager -import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.editor.Editor import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewModel @@ -46,7 +46,7 @@ object TagStatusObjectModule { fun provideFactory( @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider, - details: ObjectDetailProvider, + storage: Editor.Storage, storeOfObjectTypes: StoreOfObjectTypes, urlBuilder: UrlBuilder, setObjectDetails: UpdateDetail, @@ -58,7 +58,7 @@ object TagStatusObjectModule { ): TagStatusViewModelFactory = TagStatusViewModelFactory( params = params, values = values, - details = details, + storage = storage, relations = relations, storeOfObjectTypes = storeOfObjectTypes, urlBuilder = urlBuilder, diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt index 1a33446c9..63dd2a4e1 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt @@ -151,6 +151,7 @@ import com.anytypeio.anytype.ui.relations.RelationAddToObjectBlockFragment import com.anytypeio.anytype.ui.relations.RelationDateValueFragment import com.anytypeio.anytype.ui.relations.RelationTextValueFragment import com.anytypeio.anytype.ui.relations.RelationValueFragment +import com.anytypeio.anytype.ui.relations.value.TagStatusFragment import com.anytypeio.anytype.ui.spaces.SelectSpaceFragment import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.ARG_TEMPLATE_ID import com.google.android.material.bottomsheet.BottomSheetBehavior diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/value/TagStatusFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/value/TagStatusFragment.kt index 11540c8e2..dc3aa905d 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/relations/value/TagStatusFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/value/TagStatusFragment.kt @@ -4,19 +4,27 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.anytypeio.anytype.R import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_ui.relations.RelationsValueScreen +import com.anytypeio.anytype.core_utils.ext.argBoolean import com.anytypeio.anytype.core_utils.ext.argString import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationContext import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewModel import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewModelFactory +import com.anytypeio.anytype.ui.settings.typography import javax.inject.Inject class TagStatusFragment : BaseBottomSheetComposeFragment() { @@ -28,6 +36,8 @@ class TagStatusFragment : BaseBottomSheetComposeFragment() { private val ctx get() = argString(CTX_KEY) private val relationKey get() = argString(RELATION_KEY) private val objectId get() = argString(OBJECT_ID_KEY) + private val isLocked get() = argBoolean(IS_LOCKED_KEY) + private val relationContext get() = requireArguments().getSerializable(RELATION_CONTEXT_KEY) as RelationContext override fun onCreateView( inflater: LayoutInflater, @@ -36,11 +46,24 @@ class TagStatusFragment : BaseBottomSheetComposeFragment() { ): View = ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - RelationsValueScreen( - state = vm.viewState.collectAsStateWithLifecycle().value, - action = vm::onAction - ) + MaterialTheme( + typography = typography, + shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(10.dp)), + colors = MaterialTheme.colors.copy( + surface = colorResource(id = R.color.context_menu_background) + ) + ) { + RelationsValueScreen( + state = vm.viewState.collectAsStateWithLifecycle().value, + action = vm::onAction, + onQueryChanged = vm::onQueryChanged + ) + } } + } + + override fun onStart() { + super.onStart() vm.onStart() } @@ -48,7 +71,9 @@ class TagStatusFragment : BaseBottomSheetComposeFragment() { val params = TagStatusViewModel.Params( ctx = ctx, objectId = objectId, - relationKey = relationKey + relationKey = relationKey, + isLocked = isLocked, + relationContext = relationContext ) componentManager() .tagStatusObjectComponent.get(params) @@ -63,17 +88,23 @@ class TagStatusFragment : BaseBottomSheetComposeFragment() { fun new( ctx: Id, objectId: Id, - relationKey: Key + relationKey: Key, + isLocked: Boolean, + relationContext: RelationContext ) = TagStatusFragment().apply { arguments = bundleOf( CTX_KEY to ctx, OBJECT_ID_KEY to objectId, - RELATION_KEY to relationKey + RELATION_KEY to relationKey, + IS_LOCKED_KEY to isLocked, + RELATION_CONTEXT_KEY to relationContext ) } const val CTX_KEY = "arg.tag-status.ctx" const val RELATION_KEY = "arg.tag-status.relation.key" const val OBJECT_ID_KEY = "arg.tag-status.object" + const val IS_LOCKED_KEY = "arg.tag-status.is-locked" + const val RELATION_CONTEXT_KEY = "arg.tag-status.relation-context" } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/Common.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/Common.kt index 50c2d582c..ae536afed 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/Common.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/Common.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium import com.anytypeio.anytype.core_ui.views.Relations1 -import com.anytypeio.anytype.presentation.relations.model.RelationsListItem +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem +import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAction @Composable fun CommonContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { @@ -81,8 +83,10 @@ fun CheckedIcon(isSelected: Boolean, modifier: Modifier) { } @Composable -fun ItemTagOrStatusCreate(state: RelationsListItem.CreateItem) { - CommonContainer(modifier = Modifier.padding(top = 8.dp)) { +fun ItemTagOrStatusCreate(state: RelationsListItem.CreateItem, action: (TagStatusAction) -> Unit) { + CommonContainer(modifier = Modifier + .padding(top = 8.dp) + .noRippleClickable { action(TagStatusAction.Click(state)) }) { Row( modifier = Modifier .fillMaxWidth() diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/ItemMenu.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/ItemMenu.kt new file mode 100644 index 000000000..3da92cb69 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/ItemMenu.kt @@ -0,0 +1,75 @@ +package com.anytypeio.anytype.core_ui.relations + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.width +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.views.BodyCallout +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem +import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAction + +@Composable +fun ItemMenu( + item: RelationsListItem.Item?, + action: (TagStatusAction) -> Unit, + isMenuExpanded: MutableState +) { + if (item != null) { + DropdownMenu( + expanded = isMenuExpanded.value, + onDismissRequest = { isMenuExpanded.value = false }, + modifier = Modifier.width(220.dp) + ) { + DropdownMenuItem( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 11.dp), + onClick = { + isMenuExpanded.value = false + action(TagStatusAction.Edit(item.optionId)) + }, + ) { + Text( + text = stringResource(R.string.edit), + style = BodyCallout, + color = colorResource(id = R.color.text_primary), + ) + } + Divider(paddingEnd = 0.dp, paddingStart = 0.dp) + DropdownMenuItem( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 11.dp), + onClick = { + isMenuExpanded.value = false + action(TagStatusAction.Duplicate(item.optionId)) + } + ) { + Text( + text = stringResource(R.string.duplicate), + style = BodyCallout, + color = colorResource(id = R.color.text_primary), + ) + } + Divider(paddingEnd = 0.dp, paddingStart = 0.dp) + DropdownMenuItem( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 11.dp), + onClick = { + isMenuExpanded.value = false + action(TagStatusAction.Delete(item.optionId)) + }, + ) { + Text( + text = stringResource(R.string.delete), + style = BodyCallout, + color = colorResource(id = R.color.palette_system_red), + ) + } + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/RelationsLazyList.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/RelationsLazyList.kt index e7226544e..a0a8a0d3e 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/RelationsLazyList.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/RelationsLazyList.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.ThemeColor import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.Dragger @@ -35,14 +36,15 @@ import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable import com.anytypeio.anytype.core_ui.views.Title1 import com.anytypeio.anytype.core_ui.views.UXBody import com.anytypeio.anytype.core_ui.widgets.SearchField -import com.anytypeio.anytype.presentation.relations.RelationValueView +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAction import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewState @Composable fun RelationsValueScreen( state: TagStatusViewState, - action: (TagStatusAction) -> Unit + action: (TagStatusAction) -> Unit, + onQueryChanged: (String) -> Unit ) { Box( modifier = Modifier @@ -62,7 +64,7 @@ fun RelationsValueScreen( Header(state = state, action = action) SearchField( onFocused = {}, - onQueryChanged = { s -> } + onQueryChanged = onQueryChanged ) Divider(paddingEnd = 0.dp, paddingStart = 0.dp) RelationsLazyList(state = state, action = action) @@ -170,10 +172,10 @@ fun RelationsViewContent( items = state.items, itemContent = { _, item -> when (item) { -// is RelationValueView.Create -> ItemTagOrStatusCreate(state = item) -// is RelationValueView.Option.Status -> StatusItem(state = item) - is RelationValueView.Option.Tag -> TagItem(state = item, action = action) - else -> TODO() + is RelationsListItem.Item.Tag -> TagItem(item, action) + is RelationsListItem.Item.Status -> StatusItem(item, action) + is RelationsListItem.CreateItem.Status -> ItemTagOrStatusCreate(item, action) + is RelationsListItem.CreateItem.Tag -> ItemTagOrStatusCreate(item, action) } }) } @@ -191,7 +193,8 @@ fun RelationsViewLoading() { private fun isClearButtonVisible(state: TagStatusViewState): Boolean { if (state !is TagStatusViewState.Content) return false - return state.items.any { it is RelationValueView.Option.Tag && it.isSelected } && state.isRelationEditable + return state.items.any { it is RelationsListItem.Item.Tag && it.isSelected + || it is RelationsListItem.Item.Status && it.isSelected } && state.isRelationEditable } private fun isPlusButtonVisible(state: TagStatusViewState): Boolean { @@ -217,26 +220,19 @@ fun MyWidgetHeader() { isRelationEditable = true, title = "Tags", items = listOf( - RelationValueView.Option.Tag( + RelationsListItem.Item.Tag( name = "Urgent", - color = "red", - //number = 1, + color = ThemeColor.RED, + number = 1, isSelected = true, - id = "1", - removable = false, - isCheckboxShown = false + optionId = "1" ), - RelationValueView.Option.Tag( + RelationsListItem.Item.Tag( name = "Personal", - color = "orange", - //number = 1, + color = ThemeColor.ORANGE, + number = 2, isSelected = false, - id = "1", - removable = false, - isCheckboxShown = false - ), - RelationValueView.Create( - name = "Done" + optionId = "1" ) ) ), action = {}) diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/StatusItem.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/StatusItem.kt index 94eb5ebb7..7b409ca4d 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/StatusItem.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/StatusItem.kt @@ -10,19 +10,39 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.Divider import androidx.compose.material.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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.ThemeColor import com.anytypeio.anytype.core_ui.extensions.dark +import com.anytypeio.anytype.core_ui.foundation.noRippleCombinedClickable import com.anytypeio.anytype.core_ui.views.Relations1 -import com.anytypeio.anytype.presentation.relations.model.RelationsListItem +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem +import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAction @Composable -fun StatusItem(state: RelationsListItem.Item.Status) { - CommonContainer { +fun StatusItem( + state: RelationsListItem.Item.Status, + action: (TagStatusAction) -> Unit +) { + val haptics = LocalHapticFeedback.current + val isMenuExpanded = remember { mutableStateOf(false) } + CommonContainer( + modifier = Modifier + .noRippleCombinedClickable( + onClick = { action(TagStatusAction.Click(state)) }, + onLongClicked = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + isMenuExpanded.value = !isMenuExpanded.value + } + ) + ) { Box( modifier = Modifier .fillMaxWidth() @@ -38,13 +58,18 @@ fun StatusItem(state: RelationsListItem.Item.Status) { .align(Alignment.CenterEnd) ) Divider(modifier = Modifier.align(Alignment.BottomCenter)) + ItemMenu( + item = state, + action = action, + isMenuExpanded = isMenuExpanded + ) } } @Composable fun StatusItemText(state: RelationsListItem.Item.Status) { Text( - text = state.text, + text = state.name, color = dark(state.color), modifier = Modifier .wrapContentWidth() @@ -65,22 +90,27 @@ fun PreviewStatusItem() { ) { StatusItem( state = RelationsListItem.Item.Status( - text = "In development", + optionId = "1", + name = "In development", color = ThemeColor.RED, isSelected = true - ) + ), + action = {} ) StatusItem( state = RelationsListItem.Item.Status( - text = "Designer", + optionId = "2", + name = "Designer", color = ThemeColor.TEAL, isSelected = false - ) + ), + action = {} ) ItemTagOrStatusCreate( state = RelationsListItem.CreateItem.Status( text = "Personal" - ) + ), + action = {} ) } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagItem.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagItem.kt index d2f6311f4..5b6702d35 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagItem.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagItem.kt @@ -12,24 +12,39 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Divider import androidx.compose.material.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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.ThemeColor import com.anytypeio.anytype.core_ui.extensions.dark import com.anytypeio.anytype.core_ui.extensions.light -import com.anytypeio.anytype.core_ui.foundation.noRippleClickable +import com.anytypeio.anytype.core_ui.foundation.noRippleCombinedClickable import com.anytypeio.anytype.core_ui.views.Relations1 -import com.anytypeio.anytype.presentation.relations.RelationValueView -import com.anytypeio.anytype.presentation.relations.model.RelationsListItem +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAction @Composable -fun TagItem(state: RelationValueView.Option.Tag, action: (TagStatusAction) -> Unit) { +fun TagItem( + state: RelationsListItem.Item.Tag, + action: (TagStatusAction) -> Unit +) { + val haptics = LocalHapticFeedback.current + val isMenuExpanded = remember { mutableStateOf(false) } CommonContainer( - modifier = Modifier.noRippleClickable { action(TagStatusAction.Click(state)) } + modifier = Modifier + .noRippleCombinedClickable( + onClick = { action(TagStatusAction.Click(state)) }, + onLongClicked = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + isMenuExpanded.value = !isMenuExpanded.value + } + ) ) { Box( modifier = Modifier @@ -40,26 +55,30 @@ fun TagItem(state: RelationValueView.Option.Tag, action: (TagStatusAction) -> Un TagItemText(state = state) } CircleIcon( - //number = if (state.isSelected) state.number.toString() else null, + number = if (state.isSelected) state.number.toString() else null, isSelected = state.isSelected, modifier = Modifier - .size(36.dp) + .size(24.dp) .align(Alignment.CenterEnd) ) Divider(modifier = Modifier.align(Alignment.BottomCenter)) + ItemMenu( + item = state, + action = action, + isMenuExpanded = isMenuExpanded + ) } } @Composable -fun TagItemText(state: RelationValueView.Option.Tag) { - val themeColor = ThemeColor.values().find { it.code == state.color } ?: ThemeColor.DEFAULT +fun TagItemText(state: RelationsListItem.Item.Tag) { Text( text = state.name, - color = dark(themeColor), + color = dark(state.color), modifier = Modifier .wrapContentWidth() .background( - color = light(color = themeColor), + color = light(color = state.color), shape = RoundedCornerShape(size = 3.dp) ) .padding(start = 6.dp, end = 6.dp), @@ -78,33 +97,30 @@ fun PreviewTagItem() { .wrapContentHeight() ) { TagItem( - state = RelationValueView.Option.Tag( + state = RelationsListItem.Item.Tag( name = "Urgent", - color = "red", - //number = 1, + color = ThemeColor.RED, + number = 1, isSelected = true, - id = "1", - removable = false, - isCheckboxShown = false + optionId = "1" ), action = {} ) TagItem( - state = RelationValueView.Option.Tag( + state = RelationsListItem.Item.Tag( name = "Personal", - color = "orange", - //number = 1, + color = ThemeColor.ORANGE, + number = 1, isSelected = true, - id = "1", - removable = false, - isCheckboxShown = false + optionId = "1" ), action = {} ) ItemTagOrStatusCreate( state = RelationsListItem.CreateItem.Tag( text = "Done" - ) + ), + action = {} ) } } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/objects/options/GetOptions.kt b/domain/src/main/java/com/anytypeio/anytype/domain/objects/options/GetOptions.kt index b3350368f..e141f1ec5 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/objects/options/GetOptions.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/objects/options/GetOptions.kt @@ -63,11 +63,11 @@ class GetOptions( Relations.NAME, Relations.RELATION_OPTION_COLOR ), - fulltext = "", + fulltext = params.fulltext, ).map { ObjectWrapper.Option(it) } } - data class Params(val space: Id, val relation: Key) + data class Params(val space: Id, val relation: Key, val fulltext: String = "") } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/model/RelationsListItem.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/model/RelationsListItem.kt deleted file mode 100644 index 6453409aa..000000000 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/model/RelationsListItem.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.anytypeio.anytype.presentation.relations.model - -import com.anytypeio.anytype.core_models.ThemeColor - -sealed class RelationsListItem { - - abstract val text: String - - sealed class Item : RelationsListItem() { - data class Tag( - override val text: String, - val color: ThemeColor, - val isSelected: Boolean, - val number: Int? = null - ) : Item() - - data class Status( - override val text: String, - val color: ThemeColor, - val isSelected: Boolean - ) : Item() - } - - sealed class CreateItem( - override val text: String - ) : RelationsListItem() { - class Tag(text: String) : CreateItem(text) - class Status(text: String) : CreateItem(text) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModel.kt index 7d645d8e2..3a34622ba 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModel.kt @@ -1,27 +1,40 @@ package com.anytypeio.anytype.presentation.relations.value.tagstatus +import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.core_models.Struct +import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction +import com.anytypeio.anytype.core_utils.ext.typeOf import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.`object`.UpdateDetail import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.options.GetOptions import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.common.BaseViewModel -import com.anytypeio.anytype.presentation.relations.RelationValueView -import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.editor.Editor +import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider +import com.anytypeio.anytype.presentation.sets.filterIdsById import com.anytypeio.anytype.presentation.util.Dispatcher +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch import timber.log.Timber class TagStatusViewModel( private val params: Params, private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, - private val details: ObjectDetailProvider, + private val storage: Editor.Storage, private val storeOfObjectTypes: StoreOfObjectTypes, private val urlBuilder: UrlBuilder, private val dispatcher: Dispatcher, @@ -32,19 +45,298 @@ class TagStatusViewModel( ) : BaseViewModel() { val viewState = MutableStateFlow(TagStatusViewState.Loading) + private val query = MutableSharedFlow() + private var isRelationNotEditable = false fun onStart() { - Timber.d("TagStatusViewModel onStart, params: $params") + val obj = storage.details.current().details[params.objectId] + isRelationNotEditable = params.isLocked || storage.objectRestrictions.current() + .contains(ObjectRestriction.RELATIONS) + Timber.d("TagStatusViewModel onStart, params: $params, obj: $obj, isRelationNotEditable: $isRelationNotEditable") + viewModelScope.launch { + combine( + relations.observe( + relation = params.relationKey + ), + values.subscribe( + ctx = params.ctx, + target = params.objectId + ), + query.onStart { emit("") } + ) { relation, record, query -> + setupIsRelationNotEditable(relation) + getAllOptions( + relation = relation, + record = record, + query = query + ) + }.collect() + } + } + + fun onQueryChanged(input: String) { + viewModelScope.launch { + query.emit(input) + } } fun onAction(action: TagStatusAction) { Timber.d("TagStatusViewModel onAction, action: $action") + if (isRelationNotEditable) { + Timber.d("TagStatusViewModel onAction, relation is not editable") + sendToast("Relation is not editable") + return + } + when (action) { + TagStatusAction.Clear -> clearTagsOrStatus() + is TagStatusAction.Click -> onActionClick(action.item) + is TagStatusAction.LongClick -> { + val currentState = viewState.value + if (currentState is TagStatusViewState.Content) { + viewState.value = currentState.copy(showItemMenu = action.item) + } + } + TagStatusAction.Plus -> TODO() + is TagStatusAction.Delete -> TODO() + is TagStatusAction.Duplicate -> TODO() + is TagStatusAction.Edit -> TODO() + } + } + + private fun onActionClick(item: RelationsListItem) { + when (item) { + is RelationsListItem.Item.Status -> { + if (item.isSelected) { + clearTagsOrStatus() + } else { + addStatus(item.optionId) + } + } + is RelationsListItem.Item.Tag -> { + if (item.isSelected) { + removeTag(item.optionId) + } else { + addTag(item.optionId) + } + } + is RelationsListItem.CreateItem.Status -> TODO() + is RelationsListItem.CreateItem.Tag -> TODO() + } + } + + private suspend fun getAllOptions( + relation: ObjectWrapper.Relation, + record: Struct, + query: String + ) { + val params = GetOptions.Params( + space = spaceManager.get(), + relation = relation.key, + fulltext = query + ) + getOptions(params).proceed( + success = { options -> + Timber.d("TagStatusViewModel getAllOptions, options: ${options.size}") + initViewState( + relation = relation, + record = record, + options = options, + query = query + ) + }, + failure = { + Timber.e(it, "Error while getting options by id") + } + ) + } + + private fun initViewState( + relation: ObjectWrapper.Relation, + record: Map, + options: List, + query: String + ) { + val result = mutableListOf() + when (relation.format) { + Relation.Format.STATUS -> { + val ids: List = when (val value = record[params.relationKey]) { + is Id -> listOf(value) + is List<*> -> value.typeOf() + else -> emptyList() + } + result.addAll( + mapStatusOptions( + ids = ids, + options = options + ) + ) + } + Relation.Format.TAG -> { + val ids: List = when (val value = record[params.relationKey]) { + is Id -> listOf(value) + is List<*> -> value.typeOf() + else -> emptyList() + } + result.addAll( + mapTagOptions( + ids = ids, + options = options + ) + ) + if (query.isNotBlank()) { + result.add(RelationsListItem.CreateItem.Tag(query)) + } + } + else -> { + Timber.w("Relation format should be Tag or Status but was: ${relation.format}") + } + } + + viewState.value = if (result.isEmpty()) { + TagStatusViewState.Empty( + isRelationEditable = !isRelationNotEditable, + title = relation.name.orEmpty(), + ) + } else { + TagStatusViewState.Content( + isRelationEditable = !isRelationNotEditable, + title = relation.name.orEmpty(), + items = result + ) + } + } + + private fun addTag(tag: Id) { + viewModelScope.launch { + val obj = values.get(ctx = params.ctx, target = params.objectId) + val result = mutableListOf() + val value = obj[params.relationKey] + if (value is List<*>) { + result.addAll(value.typeOf()) + } else if (value is Id) { + result.add(value) + } + result.add(tag) + setObjectDetails( + UpdateDetail.Params( + target = params.objectId, + key = params.relationKey, + value = result + ) + ).process( + failure = { Timber.e(it, "Error while adding tag") }, + success = { + dispatcher.send(it) + sendAnalyticsRelationValueEvent(analytics) + } + ) + } + } + + private fun removeTag(tag: Id) { + viewModelScope.launch { + val obj = values.get(ctx = params.ctx, target = params.objectId) + val remaining = obj[params.relationKey].filterIdsById(tag) + setObjectDetails( + UpdateDetail.Params( + target = params.objectId, + key = params.relationKey, + value = remaining + ) + ).process( + failure = { Timber.e(it, "Error while adding tag") }, + success = { + dispatcher.send(it) + sendAnalyticsRelationValueEvent(analytics) + }) + } + } + + private fun addStatus(status: Id) { + viewModelScope.launch { + setObjectDetails( + UpdateDetail.Params( + target = params.objectId, + key = params.relationKey, + value = listOf(status) + ) + ).process( + failure = { Timber.e(it, "Error while adding tag") }, + success = { + dispatcher.send(it) + sendAnalyticsRelationValueEvent(analytics) + } + ) + } + } + + private fun clearTagsOrStatus() { + viewModelScope.launch { + setObjectDetails( + UpdateDetail.Params( + target = params.objectId, + key = params.relationKey, + value = null + ) + ).process( + failure = { Timber.e(it, "Error while clearing tags or select") }, + success = { + dispatcher.send(it) + sendAnalyticsRelationValueEvent(analytics) + }) + } + } + + private fun mapTagOptions( + ids: List, + options: List + ) = options.map { option -> + val index = ids.indexOf(option.id) + val isSelected = index != -1 + val number = if (isSelected) index + 1 else Int.MAX_VALUE + RelationsListItem.Item.Tag( + optionId = option.id, + name = option.name.orEmpty(), + color = getOrDefault(option.color), + isSelected = isSelected, + number = number + ) + }.sortedBy { it.number } + + private fun mapStatusOptions( + ids: List, + options: List + ) = options.map { option -> + val index = ids.indexOf(option.id) + val isSelected = index != -1 + RelationsListItem.Item.Status( + optionId = option.id, + name = option.name.orEmpty(), + color = getOrDefault(option.color), + isSelected = isSelected + ) + } + + private fun getOrDefault(code: String?): ThemeColor { + return ThemeColor.values().find { it.code == code } ?: ThemeColor.DEFAULT + } + + private fun setupIsRelationNotEditable(relation: ObjectWrapper.Relation) { + isRelationNotEditable = params.isLocked + || storage.objectRestrictions.current().contains(ObjectRestriction.RELATIONS) + || relation.isReadonlyValue + || relation.isHidden == true + || relation.isDeleted == true + || relation.isArchived == true + || !relation.isValid } data class Params( val ctx: Id, val objectId: Id, - val relationKey: Id + val relationKey: Id, + val isLocked: Boolean, + val relationContext: RelationContext ) } @@ -56,14 +348,50 @@ sealed class TagStatusViewState { data class Content( val title: String, - val items: List, - val isRelationEditable: Boolean + val items: List, + val isRelationEditable: Boolean, + val showItemMenu: RelationsListItem.Item? = null ) : TagStatusViewState() } sealed class TagStatusAction { - data class Click(val item: RelationValueView) : TagStatusAction() - data class LongClick(val item: RelationValueView) : TagStatusAction() + data class Click(val item: RelationsListItem) : TagStatusAction() + data class LongClick(val item: RelationsListItem.Item) : TagStatusAction() object Clear : TagStatusAction() object Plus : TagStatusAction() + data class Edit(val optionId: Id) : TagStatusAction() + data class Delete(val optionId: Id) : TagStatusAction() + data class Duplicate(val optionId: Id) : TagStatusAction() +} + +enum class RelationContext{ OBJECT, OBJECT_SET, DATA_VIEW } + +sealed class RelationsListItem { + + sealed class Item : RelationsListItem() { + + abstract val optionId: Id + data class Tag( + override val optionId: Id, + val name: String, + val color: ThemeColor, + val isSelected: Boolean, + val number: Int = Int.MAX_VALUE, + val showMenu: Boolean = false + ) : Item() + + data class Status( + override val optionId: Id, + val name: String, + val color: ThemeColor, + val isSelected: Boolean + ) : Item() + } + + sealed class CreateItem( + val text: String + ) : RelationsListItem() { + class Tag(text: String) : CreateItem(text) + class Status(text: String) : CreateItem(text) + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModelFactory.kt index ea86ca23b..019f420ee 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModelFactory.kt @@ -9,7 +9,7 @@ import com.anytypeio.anytype.domain.`object`.UpdateDetail import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.options.GetOptions import com.anytypeio.anytype.domain.workspace.SpaceManager -import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.editor.Editor import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import com.anytypeio.anytype.presentation.util.Dispatcher @@ -19,7 +19,7 @@ class TagStatusViewModelFactory @Inject constructor( private val params: TagStatusViewModel.Params, private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, - private val details: ObjectDetailProvider, + private val storage: Editor.Storage, private val storeOfObjectTypes: StoreOfObjectTypes, private val urlBuilder: UrlBuilder, private val dispatcher: Dispatcher, @@ -35,7 +35,7 @@ class TagStatusViewModelFactory @Inject constructor( params = params, relations = relations, values = values, - details = details, + storage = storage, storeOfObjectTypes = storeOfObjectTypes, dispatcher = dispatcher, setObjectDetails = setObjectDetails,