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.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,

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.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

View File

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

View File

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

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.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 = {})

View File

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

View File

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

View File

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

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
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<Payload>,
@ -32,19 +45,298 @@ class TagStatusViewModel(
) : BaseViewModel() {
val viewState = MutableStateFlow<TagStatusViewState>(TagStatusViewState.Loading)
private val query = MutableSharedFlow<String>()
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<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(
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<RelationValueView>,
val isRelationEditable: Boolean
val items: List<RelationsListItem>,
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)
}
}

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.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<Payload>,
@ -35,7 +35,7 @@ class TagStatusViewModelFactory @Inject constructor(
params = params,
relations = relations,
values = values,
details = details,
storage = storage,
storeOfObjectTypes = storeOfObjectTypes,
dispatcher = dispatcher,
setObjectDetails = setObjectDetails,