DROID-2121 Relations | Tags & status screen logic (#817)

This commit is contained in:
Konstantin Ivanov 2024-02-01 15:56:38 +01:00 committed by GitHub
parent 6b7d95b9e7
commit d4d1b3ea3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 564 additions and 113 deletions

View File

@ -8,7 +8,7 @@ import com.anytypeio.anytype.domain.`object`.UpdateDetail
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.options.GetOptions import com.anytypeio.anytype.domain.objects.options.GetOptions
import com.anytypeio.anytype.domain.workspace.SpaceManager 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.ObjectRelationProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider
import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewModel import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewModel
@ -46,7 +46,7 @@ object TagStatusObjectModule {
fun provideFactory( fun provideFactory(
@Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider,
@Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider, @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider,
details: ObjectDetailProvider, storage: Editor.Storage,
storeOfObjectTypes: StoreOfObjectTypes, storeOfObjectTypes: StoreOfObjectTypes,
urlBuilder: UrlBuilder, urlBuilder: UrlBuilder,
setObjectDetails: UpdateDetail, setObjectDetails: UpdateDetail,
@ -58,7 +58,7 @@ object TagStatusObjectModule {
): TagStatusViewModelFactory = TagStatusViewModelFactory( ): TagStatusViewModelFactory = TagStatusViewModelFactory(
params = params, params = params,
values = values, values = values,
details = details, storage = storage,
relations = relations, relations = relations,
storeOfObjectTypes = storeOfObjectTypes, storeOfObjectTypes = storeOfObjectTypes,
urlBuilder = urlBuilder, urlBuilder = urlBuilder,

View File

@ -151,6 +151,7 @@ import com.anytypeio.anytype.ui.relations.RelationAddToObjectBlockFragment
import com.anytypeio.anytype.ui.relations.RelationDateValueFragment import com.anytypeio.anytype.ui.relations.RelationDateValueFragment
import com.anytypeio.anytype.ui.relations.RelationTextValueFragment import com.anytypeio.anytype.ui.relations.RelationTextValueFragment
import com.anytypeio.anytype.ui.relations.RelationValueFragment 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.spaces.SelectSpaceFragment
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.ARG_TEMPLATE_ID import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.ARG_TEMPLATE_ID
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior

View File

@ -4,19 +4,27 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy 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.core.os.bundleOf
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_ui.relations.RelationsValueScreen 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.ext.argString
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager 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.TagStatusViewModel
import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewModelFactory import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewModelFactory
import com.anytypeio.anytype.ui.settings.typography
import javax.inject.Inject import javax.inject.Inject
class TagStatusFragment : BaseBottomSheetComposeFragment() { class TagStatusFragment : BaseBottomSheetComposeFragment() {
@ -28,6 +36,8 @@ class TagStatusFragment : BaseBottomSheetComposeFragment() {
private val ctx get() = argString(CTX_KEY) private val ctx get() = argString(CTX_KEY)
private val relationKey get() = argString(RELATION_KEY) private val relationKey get() = argString(RELATION_KEY)
private val objectId get() = argString(OBJECT_ID_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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -36,11 +46,24 @@ class TagStatusFragment : BaseBottomSheetComposeFragment() {
): View = ComposeView(requireContext()).apply { ): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { setContent {
RelationsValueScreen( MaterialTheme(
state = vm.viewState.collectAsStateWithLifecycle().value, typography = typography,
action = vm::onAction 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() vm.onStart()
} }
@ -48,7 +71,9 @@ class TagStatusFragment : BaseBottomSheetComposeFragment() {
val params = TagStatusViewModel.Params( val params = TagStatusViewModel.Params(
ctx = ctx, ctx = ctx,
objectId = objectId, objectId = objectId,
relationKey = relationKey relationKey = relationKey,
isLocked = isLocked,
relationContext = relationContext
) )
componentManager() componentManager()
.tagStatusObjectComponent.get(params) .tagStatusObjectComponent.get(params)
@ -63,17 +88,23 @@ class TagStatusFragment : BaseBottomSheetComposeFragment() {
fun new( fun new(
ctx: Id, ctx: Id,
objectId: Id, objectId: Id,
relationKey: Key relationKey: Key,
isLocked: Boolean,
relationContext: RelationContext
) = TagStatusFragment().apply { ) = TagStatusFragment().apply {
arguments = bundleOf( arguments = bundleOf(
CTX_KEY to ctx, CTX_KEY to ctx,
OBJECT_ID_KEY to objectId, 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 CTX_KEY = "arg.tag-status.ctx"
const val RELATION_KEY = "arg.tag-status.relation.key" const val RELATION_KEY = "arg.tag-status.relation.key"
const val OBJECT_ID_KEY = "arg.tag-status.object" 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"
} }
} }

View File

@ -22,9 +22,11 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.R 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.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations1 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 @Composable
fun CommonContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { fun CommonContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
@ -81,8 +83,10 @@ fun CheckedIcon(isSelected: Boolean, modifier: Modifier) {
} }
@Composable @Composable
fun ItemTagOrStatusCreate(state: RelationsListItem.CreateItem) { fun ItemTagOrStatusCreate(state: RelationsListItem.CreateItem, action: (TagStatusAction) -> Unit) {
CommonContainer(modifier = Modifier.padding(top = 8.dp)) { CommonContainer(modifier = Modifier
.padding(top = 8.dp)
.noRippleClickable { action(TagStatusAction.Click(state)) }) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -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<Boolean>
) {
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),
)
}
}
}
}

View File

@ -27,6 +27,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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.R
import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Dragger 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.Title1
import com.anytypeio.anytype.core_ui.views.UXBody import com.anytypeio.anytype.core_ui.views.UXBody
import com.anytypeio.anytype.core_ui.widgets.SearchField 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.TagStatusAction
import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewState import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewState
@Composable @Composable
fun RelationsValueScreen( fun RelationsValueScreen(
state: TagStatusViewState, state: TagStatusViewState,
action: (TagStatusAction) -> Unit action: (TagStatusAction) -> Unit,
onQueryChanged: (String) -> Unit
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@ -62,7 +64,7 @@ fun RelationsValueScreen(
Header(state = state, action = action) Header(state = state, action = action)
SearchField( SearchField(
onFocused = {}, onFocused = {},
onQueryChanged = { s -> } onQueryChanged = onQueryChanged
) )
Divider(paddingEnd = 0.dp, paddingStart = 0.dp) Divider(paddingEnd = 0.dp, paddingStart = 0.dp)
RelationsLazyList(state = state, action = action) RelationsLazyList(state = state, action = action)
@ -170,10 +172,10 @@ fun RelationsViewContent(
items = state.items, items = state.items,
itemContent = { _, item -> itemContent = { _, item ->
when (item) { when (item) {
// is RelationValueView.Create -> ItemTagOrStatusCreate(state = item) is RelationsListItem.Item.Tag -> TagItem(item, action)
// is RelationValueView.Option.Status -> StatusItem(state = item) is RelationsListItem.Item.Status -> StatusItem(item, action)
is RelationValueView.Option.Tag -> TagItem(state = item, action = action) is RelationsListItem.CreateItem.Status -> ItemTagOrStatusCreate(item, action)
else -> TODO() is RelationsListItem.CreateItem.Tag -> ItemTagOrStatusCreate(item, action)
} }
}) })
} }
@ -191,7 +193,8 @@ fun RelationsViewLoading() {
private fun isClearButtonVisible(state: TagStatusViewState): Boolean { private fun isClearButtonVisible(state: TagStatusViewState): Boolean {
if (state !is TagStatusViewState.Content) return false 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 { private fun isPlusButtonVisible(state: TagStatusViewState): Boolean {
@ -217,26 +220,19 @@ fun MyWidgetHeader() {
isRelationEditable = true, isRelationEditable = true,
title = "Tags", title = "Tags",
items = listOf( items = listOf(
RelationValueView.Option.Tag( RelationsListItem.Item.Tag(
name = "Urgent", name = "Urgent",
color = "red", color = ThemeColor.RED,
//number = 1, number = 1,
isSelected = true, isSelected = true,
id = "1", optionId = "1"
removable = false,
isCheckboxShown = false
), ),
RelationValueView.Option.Tag( RelationsListItem.Item.Tag(
name = "Personal", name = "Personal",
color = "orange", color = ThemeColor.ORANGE,
//number = 1, number = 2,
isSelected = false, isSelected = false,
id = "1", optionId = "1"
removable = false,
isCheckboxShown = false
),
RelationValueView.Create(
name = "Done"
) )
) )
), action = {}) ), action = {})

View File

@ -10,19 +10,39 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.ThemeColor import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_ui.extensions.dark 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.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 @Composable
fun StatusItem(state: RelationsListItem.Item.Status) { fun StatusItem(
CommonContainer { 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( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -38,13 +58,18 @@ fun StatusItem(state: RelationsListItem.Item.Status) {
.align(Alignment.CenterEnd) .align(Alignment.CenterEnd)
) )
Divider(modifier = Modifier.align(Alignment.BottomCenter)) Divider(modifier = Modifier.align(Alignment.BottomCenter))
ItemMenu(
item = state,
action = action,
isMenuExpanded = isMenuExpanded
)
} }
} }
@Composable @Composable
fun StatusItemText(state: RelationsListItem.Item.Status) { fun StatusItemText(state: RelationsListItem.Item.Status) {
Text( Text(
text = state.text, text = state.name,
color = dark(state.color), color = dark(state.color),
modifier = Modifier modifier = Modifier
.wrapContentWidth() .wrapContentWidth()
@ -65,22 +90,27 @@ fun PreviewStatusItem() {
) { ) {
StatusItem( StatusItem(
state = RelationsListItem.Item.Status( state = RelationsListItem.Item.Status(
text = "In development", optionId = "1",
name = "In development",
color = ThemeColor.RED, color = ThemeColor.RED,
isSelected = true isSelected = true
) ),
action = {}
) )
StatusItem( StatusItem(
state = RelationsListItem.Item.Status( state = RelationsListItem.Item.Status(
text = "Designer", optionId = "2",
name = "Designer",
color = ThemeColor.TEAL, color = ThemeColor.TEAL,
isSelected = false isSelected = false
) ),
action = {}
) )
ItemTagOrStatusCreate( ItemTagOrStatusCreate(
state = RelationsListItem.CreateItem.Status( state = RelationsListItem.CreateItem.Status(
text = "Personal" text = "Personal"
) ),
action = {}
) )
} }
} }

View File

@ -12,24 +12,39 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.ThemeColor import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_ui.extensions.dark import com.anytypeio.anytype.core_ui.extensions.dark
import com.anytypeio.anytype.core_ui.extensions.light 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.core_ui.views.Relations1
import com.anytypeio.anytype.presentation.relations.RelationValueView import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem
import com.anytypeio.anytype.presentation.relations.model.RelationsListItem
import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAction import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAction
@Composable @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( 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( Box(
modifier = Modifier modifier = Modifier
@ -40,26 +55,30 @@ fun TagItem(state: RelationValueView.Option.Tag, action: (TagStatusAction) -> Un
TagItemText(state = state) TagItemText(state = state)
} }
CircleIcon( CircleIcon(
//number = if (state.isSelected) state.number.toString() else null, number = if (state.isSelected) state.number.toString() else null,
isSelected = state.isSelected, isSelected = state.isSelected,
modifier = Modifier modifier = Modifier
.size(36.dp) .size(24.dp)
.align(Alignment.CenterEnd) .align(Alignment.CenterEnd)
) )
Divider(modifier = Modifier.align(Alignment.BottomCenter)) Divider(modifier = Modifier.align(Alignment.BottomCenter))
ItemMenu(
item = state,
action = action,
isMenuExpanded = isMenuExpanded
)
} }
} }
@Composable @Composable
fun TagItemText(state: RelationValueView.Option.Tag) { fun TagItemText(state: RelationsListItem.Item.Tag) {
val themeColor = ThemeColor.values().find { it.code == state.color } ?: ThemeColor.DEFAULT
Text( Text(
text = state.name, text = state.name,
color = dark(themeColor), color = dark(state.color),
modifier = Modifier modifier = Modifier
.wrapContentWidth() .wrapContentWidth()
.background( .background(
color = light(color = themeColor), color = light(color = state.color),
shape = RoundedCornerShape(size = 3.dp) shape = RoundedCornerShape(size = 3.dp)
) )
.padding(start = 6.dp, end = 6.dp), .padding(start = 6.dp, end = 6.dp),
@ -78,33 +97,30 @@ fun PreviewTagItem() {
.wrapContentHeight() .wrapContentHeight()
) { ) {
TagItem( TagItem(
state = RelationValueView.Option.Tag( state = RelationsListItem.Item.Tag(
name = "Urgent", name = "Urgent",
color = "red", color = ThemeColor.RED,
//number = 1, number = 1,
isSelected = true, isSelected = true,
id = "1", optionId = "1"
removable = false,
isCheckboxShown = false
), ),
action = {} action = {}
) )
TagItem( TagItem(
state = RelationValueView.Option.Tag( state = RelationsListItem.Item.Tag(
name = "Personal", name = "Personal",
color = "orange", color = ThemeColor.ORANGE,
//number = 1, number = 1,
isSelected = true, isSelected = true,
id = "1", optionId = "1"
removable = false,
isCheckboxShown = false
), ),
action = {} action = {}
) )
ItemTagOrStatusCreate( ItemTagOrStatusCreate(
state = RelationsListItem.CreateItem.Tag( state = RelationsListItem.CreateItem.Tag(
text = "Done" text = "Done"
) ),
action = {}
) )
} }
} }

View File

@ -63,11 +63,11 @@ class GetOptions(
Relations.NAME, Relations.NAME,
Relations.RELATION_OPTION_COLOR Relations.RELATION_OPTION_COLOR
), ),
fulltext = "", fulltext = params.fulltext,
).map { ).map {
ObjectWrapper.Option(it) ObjectWrapper.Option(it)
} }
} }
data class Params(val space: Id, val relation: Key) data class Params(val space: Id, val relation: Key, val fulltext: String = "")
} }

View File

@ -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)
}
}

View File

@ -1,27 +1,40 @@
package com.anytypeio.anytype.presentation.relations.value.tagstatus package com.anytypeio.anytype.presentation.relations.value.tagstatus
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id 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.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.misc.UrlBuilder
import com.anytypeio.anytype.domain.`object`.UpdateDetail import com.anytypeio.anytype.domain.`object`.UpdateDetail
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.options.GetOptions import com.anytypeio.anytype.domain.objects.options.GetOptions
import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.relations.RelationValueView import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider
import com.anytypeio.anytype.presentation.sets.filterIdsById
import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow 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 import timber.log.Timber
class TagStatusViewModel( class TagStatusViewModel(
private val params: Params, private val params: Params,
private val relations: ObjectRelationProvider, private val relations: ObjectRelationProvider,
private val values: ObjectValueProvider, private val values: ObjectValueProvider,
private val details: ObjectDetailProvider, private val storage: Editor.Storage,
private val storeOfObjectTypes: StoreOfObjectTypes, private val storeOfObjectTypes: StoreOfObjectTypes,
private val urlBuilder: UrlBuilder, private val urlBuilder: UrlBuilder,
private val dispatcher: Dispatcher<Payload>, private val dispatcher: Dispatcher<Payload>,
@ -32,19 +45,298 @@ class TagStatusViewModel(
) : BaseViewModel() { ) : BaseViewModel() {
val viewState = MutableStateFlow<TagStatusViewState>(TagStatusViewState.Loading) val viewState = MutableStateFlow<TagStatusViewState>(TagStatusViewState.Loading)
private val query = MutableSharedFlow<String>()
private var isRelationNotEditable = false
fun onStart() { 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) { fun onAction(action: TagStatusAction) {
Timber.d("TagStatusViewModel onAction, action: $action") 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<String, Any?>,
options: List<ObjectWrapper.Option>,
query: String
) {
val result = mutableListOf<RelationsListItem>()
when (relation.format) {
Relation.Format.STATUS -> {
val ids: List<Id> = 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<Id> = 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<Id>()
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<Id>,
options: List<ObjectWrapper.Option>
) = 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<Id>,
options: List<ObjectWrapper.Option>
) = 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( data class Params(
val ctx: Id, val ctx: Id,
val objectId: 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( data class Content(
val title: String, val title: String,
val items: List<RelationValueView>, val items: List<RelationsListItem>,
val isRelationEditable: Boolean val isRelationEditable: Boolean,
val showItemMenu: RelationsListItem.Item? = null
) : TagStatusViewState() ) : TagStatusViewState()
} }
sealed class TagStatusAction { sealed class TagStatusAction {
data class Click(val item: RelationValueView) : TagStatusAction() data class Click(val item: RelationsListItem) : TagStatusAction()
data class LongClick(val item: RelationValueView) : TagStatusAction() data class LongClick(val item: RelationsListItem.Item) : TagStatusAction()
object Clear : TagStatusAction() object Clear : TagStatusAction()
object Plus : 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)
}
} }

View File

@ -9,7 +9,7 @@ import com.anytypeio.anytype.domain.`object`.UpdateDetail
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.options.GetOptions import com.anytypeio.anytype.domain.objects.options.GetOptions
import com.anytypeio.anytype.domain.workspace.SpaceManager 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.ObjectRelationProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider
import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.presentation.util.Dispatcher
@ -19,7 +19,7 @@ class TagStatusViewModelFactory @Inject constructor(
private val params: TagStatusViewModel.Params, private val params: TagStatusViewModel.Params,
private val relations: ObjectRelationProvider, private val relations: ObjectRelationProvider,
private val values: ObjectValueProvider, private val values: ObjectValueProvider,
private val details: ObjectDetailProvider, private val storage: Editor.Storage,
private val storeOfObjectTypes: StoreOfObjectTypes, private val storeOfObjectTypes: StoreOfObjectTypes,
private val urlBuilder: UrlBuilder, private val urlBuilder: UrlBuilder,
private val dispatcher: Dispatcher<Payload>, private val dispatcher: Dispatcher<Payload>,
@ -35,7 +35,7 @@ class TagStatusViewModelFactory @Inject constructor(
params = params, params = params,
relations = relations, relations = relations,
values = values, values = values,
details = details, storage = storage,
storeOfObjectTypes = storeOfObjectTypes, storeOfObjectTypes = storeOfObjectTypes,
dispatcher = dispatcher, dispatcher = dispatcher,
setObjectDetails = setObjectDetails, setObjectDetails = setObjectDetails,