mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-642 Relations | Feature | Suggest marketplace relations for user input search query + allow installing relation to user workspace (#2750)
This commit is contained in:
parent
9ea7710e9c
commit
c52bd3d44b
17 changed files with 766 additions and 191 deletions
|
@ -3,12 +3,15 @@ package com.anytypeio.anytype.di.feature.relations
|
|||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.core_utils.di.scope.PerDialog
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.dataview.interactor.AddRelationToDataView
|
||||
import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfRelations
|
||||
import com.anytypeio.anytype.domain.relations.AddRelationToObject
|
||||
import com.anytypeio.anytype.domain.relations.GetRelations
|
||||
import com.anytypeio.anytype.domain.relations.ObjectRelationList
|
||||
import com.anytypeio.anytype.domain.workspace.AddObjectToWorkspace
|
||||
import com.anytypeio.anytype.presentation.relations.RelationAddToDataViewViewModel
|
||||
import com.anytypeio.anytype.presentation.relations.RelationAddToObjectViewModel
|
||||
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
|
||||
|
@ -21,6 +24,7 @@ import com.anytypeio.anytype.ui.relations.RelationAddToObjectFragment
|
|||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Subcomponent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Subcomponent(modules = [RelationAddToObjectModule::class])
|
||||
|
@ -48,12 +52,18 @@ object RelationAddToObjectModule {
|
|||
dispatcher: Dispatcher<Payload>,
|
||||
analytics: Analytics,
|
||||
relationsProvider: ObjectRelationProvider,
|
||||
getRelations: GetRelations,
|
||||
appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
addObjectToWorkspace: AddObjectToWorkspace
|
||||
): RelationAddToObjectViewModel.Factory = RelationAddToObjectViewModel.Factory(
|
||||
storeOfRelations = storeOfRelations,
|
||||
addRelationToObject = addRelationToObject,
|
||||
dispatcher = dispatcher,
|
||||
analytics = analytics,
|
||||
relationsProvider = relationsProvider,
|
||||
getRelations = getRelations,
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
addObjectToWorkspace = addObjectToWorkspace
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
|
@ -62,6 +72,31 @@ object RelationAddToObjectModule {
|
|||
fun provideObjectRelationListUseCase(
|
||||
repo: BlockRepository
|
||||
): ObjectRelationList = ObjectRelationList(repo)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerDialog
|
||||
fun getRelations(repo: BlockRepository) : GetRelations = GetRelations(repo)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerDialog
|
||||
fun appCoroutineDispatchers() : AppCoroutineDispatchers = AppCoroutineDispatchers(
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerDialog
|
||||
fun addObjectToWorkspace(
|
||||
repo: BlockRepository,
|
||||
appCoroutineDispatchers: AppCoroutineDispatchers
|
||||
) : AddObjectToWorkspace = AddObjectToWorkspace(
|
||||
repo = repo,
|
||||
dispatchers = appCoroutineDispatchers
|
||||
)
|
||||
}
|
||||
|
||||
@Subcomponent(modules = [RelationAddToDataViewModule::class])
|
||||
|
@ -79,27 +114,32 @@ interface RelationAddToDataViewSubComponent {
|
|||
|
||||
@Module
|
||||
object RelationAddToDataViewModule {
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerDialog
|
||||
fun provideViewModelFactory(
|
||||
addRelationToDataView: AddRelationToDataView,
|
||||
storeOfRelations: StoreOfRelations,
|
||||
dispatcher: Dispatcher<Payload>,
|
||||
state: StateFlow<ObjectSet>,
|
||||
session: ObjectSetSession,
|
||||
updateDataViewViewer: UpdateDataViewViewer,
|
||||
analytics: Analytics,
|
||||
relationsProvider: ObjectRelationProvider,
|
||||
appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
getRelations: GetRelations,
|
||||
addObjectToWorkspace: AddObjectToWorkspace
|
||||
): RelationAddToDataViewViewModel.Factory = RelationAddToDataViewViewModel.Factory(
|
||||
storeOfRelations = storeOfRelations,
|
||||
addRelationToDataView = addRelationToDataView,
|
||||
dispatcher = dispatcher,
|
||||
state = state,
|
||||
session = session,
|
||||
updateDataViewViewer = updateDataViewViewer,
|
||||
analytics = analytics,
|
||||
relationsProvider = relationsProvider
|
||||
relationsProvider = relationsProvider,
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
getRelations = getRelations,
|
||||
addObjectToWorkspace = addObjectToWorkspace
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
|
@ -115,4 +155,29 @@ object RelationAddToDataViewModule {
|
|||
fun provideAddRelationToDataViewUseCase(
|
||||
repo: BlockRepository
|
||||
): AddRelationToDataView = AddRelationToDataView(repo)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerDialog
|
||||
fun getRelations(repo: BlockRepository) : GetRelations = GetRelations(repo)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerDialog
|
||||
fun appCoroutineDispatchers() : AppCoroutineDispatchers = AppCoroutineDispatchers(
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerDialog
|
||||
fun addObjectToWorkspace(
|
||||
repo: BlockRepository,
|
||||
appCoroutineDispatchers: AppCoroutineDispatchers
|
||||
) : AddObjectToWorkspace = AddObjectToWorkspace(
|
||||
repo = repo,
|
||||
dispatchers = appCoroutineDispatchers
|
||||
)
|
||||
}
|
|
@ -17,6 +17,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.analytics.base.EventsDictionary
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.core_ui.features.relations.RelationAddAdapter
|
||||
import com.anytypeio.anytype.core_ui.features.relations.RelationAddHeaderAdapter
|
||||
import com.anytypeio.anytype.core_ui.reactive.focusChanges
|
||||
|
@ -34,7 +36,7 @@ import com.anytypeio.anytype.di.common.componentManager
|
|||
import com.anytypeio.anytype.presentation.relations.RelationAddToDataViewViewModel
|
||||
import com.anytypeio.anytype.presentation.relations.RelationAddToObjectViewModel
|
||||
import com.anytypeio.anytype.presentation.relations.RelationAddViewModelBase
|
||||
import com.anytypeio.anytype.presentation.relations.model.RelationView
|
||||
import com.anytypeio.anytype.presentation.relations.RelationAddViewModelBase.Command
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import java.io.Serializable
|
||||
import javax.inject.Inject
|
||||
|
@ -56,7 +58,10 @@ abstract class RelationAddBaseFragment : BaseBottomSheetTextInputFragment<Fragme
|
|||
}
|
||||
|
||||
private val relationAdapter = RelationAddAdapter { relation ->
|
||||
onRelationSelected(ctx = ctx, relation = relation)
|
||||
vm.onRelationSelected(
|
||||
ctx = ctx,
|
||||
relation = relation
|
||||
)
|
||||
}
|
||||
|
||||
private val concatAdapter = ConcatAdapter(createFromScratchAdapter, relationAdapter)
|
||||
|
@ -94,6 +99,17 @@ abstract class RelationAddBaseFragment : BaseBottomSheetTextInputFragment<Fragme
|
|||
subscribe(searchRelationInput.textChanges()) {
|
||||
if (it.isEmpty()) clearSearchText.invisible() else clearSearchText.visible()
|
||||
}
|
||||
subscribe(vm.command) { command ->
|
||||
when(command) {
|
||||
is Command.DispatchSelectedRelation -> {
|
||||
onRelationSelected(
|
||||
ctx = command.ctx,
|
||||
relation = command.relation,
|
||||
format = command.format
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,12 +124,7 @@ abstract class RelationAddBaseFragment : BaseBottomSheetTextInputFragment<Fragme
|
|||
BottomSheetBehavior.from(root.parent as View).state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
vm.onStart()
|
||||
}
|
||||
|
||||
abstract fun onRelationSelected(ctx: Id, relation: RelationView.Existing)
|
||||
abstract fun onRelationSelected(ctx: Id, relation: Key, format: RelationFormat)
|
||||
abstract fun onCreateFromScratchClicked()
|
||||
|
||||
override fun inflateBinding(
|
||||
|
@ -136,10 +147,11 @@ class RelationAddToObjectFragment : RelationAddBaseFragment() {
|
|||
lateinit var factory: RelationAddToObjectViewModel.Factory
|
||||
override val vm: RelationAddToObjectViewModel by viewModels { factory }
|
||||
|
||||
override fun onRelationSelected(ctx: Id, relation: RelationView.Existing) {
|
||||
override fun onRelationSelected(ctx: Id, relation: Key, format: RelationFormat) {
|
||||
vm.onRelationSelected(
|
||||
ctx = ctx,
|
||||
relation = relation,
|
||||
format = format,
|
||||
screenType = EventsDictionary.Type.menu
|
||||
)
|
||||
}
|
||||
|
@ -177,10 +189,11 @@ class RelationAddToDataViewFragment : RelationAddBaseFragment() {
|
|||
lateinit var factory: RelationAddToDataViewViewModel.Factory
|
||||
override val vm: RelationAddToDataViewViewModel by viewModels { factory }
|
||||
|
||||
override fun onRelationSelected(ctx: Id, relation: RelationView.Existing) {
|
||||
override fun onRelationSelected(ctx: Id, relation: Key, format: RelationFormat) {
|
||||
vm.onRelationSelected(
|
||||
ctx = ctx,
|
||||
relation = relation,
|
||||
format = format,
|
||||
dv = dv,
|
||||
screenType = EventsDictionary.Type.dataView
|
||||
)
|
||||
|
@ -235,10 +248,11 @@ class RelationAddToObjectBlockFragment : RelationAddBaseFragment() {
|
|||
super.onStart()
|
||||
}
|
||||
|
||||
override fun onRelationSelected(ctx: Id, relation: RelationView.Existing) {
|
||||
override fun onRelationSelected(ctx: Id, relation: Key, format: RelationFormat) {
|
||||
vm.onRelationSelected(
|
||||
ctx = ctx,
|
||||
relation = relation,
|
||||
format = format,
|
||||
screenType = EventsDictionary.Type.block
|
||||
)
|
||||
}
|
||||
|
|
|
@ -43,4 +43,8 @@ object MarketplaceObjectTypeIds {
|
|||
const val BOOKMARK = "_otbookmark"
|
||||
|
||||
const val MARKETPLACE_OBJECT_TYPE_PREFIX = "_ot"
|
||||
}
|
||||
|
||||
object Marketplace {
|
||||
const val MARKETPLACE_ID = "_anytype_marketplace"
|
||||
}
|
|
@ -181,6 +181,8 @@ sealed class ObjectWrapper {
|
|||
|
||||
val id: Id by default
|
||||
val key: Key get() = relationKey
|
||||
val workspaceId: Id? by default
|
||||
val sourceObject: Id? by default
|
||||
val format: RelationFormat get() = relationFormat
|
||||
val name: String? by default
|
||||
val isHidden: Boolean? by default
|
||||
|
|
|
@ -4,7 +4,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.anytypeio.anytype.core_ui.databinding.ItemDefaultListSectionBinding
|
||||
|
||||
class DefaultSectionViewHolder(
|
||||
private val binding: ItemDefaultListSectionBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(name: String) { binding.tvSectionName.text = name }
|
||||
val binding: ItemDefaultListSectionBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(name: String) {
|
||||
binding.tvSectionName.text = name
|
||||
}
|
||||
}
|
|
@ -2,54 +2,112 @@ package com.anytypeio.anytype.core_ui.features.relations
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.databinding.*
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultSectionViewHolder
|
||||
import com.anytypeio.anytype.core_ui.databinding.ItemDefaultListSectionBinding
|
||||
import com.anytypeio.anytype.core_ui.databinding.ItemRelationCreateFromScratchBinding
|
||||
import com.anytypeio.anytype.core_ui.databinding.ItemRelationCreateFromScratchConnectWithBinding
|
||||
import com.anytypeio.anytype.core_ui.databinding.ItemRelationCreateFromScratchLimitObjectTypesBinding
|
||||
import com.anytypeio.anytype.core_ui.databinding.ItemRelationCreateFromScratchNameInputBinding
|
||||
import com.anytypeio.anytype.core_ui.databinding.ItemRelationFormatBinding
|
||||
import com.anytypeio.anytype.core_ui.databinding.ItemRelationFormatCreateFromScratchBinding
|
||||
import com.anytypeio.anytype.core_ui.extensions.getPrettyName
|
||||
import com.anytypeio.anytype.core_ui.features.relations.holders.DefaultRelationFormatViewHolder
|
||||
import com.anytypeio.anytype.core_ui.features.relations.holders.DefaultRelationViewHolder
|
||||
import com.anytypeio.anytype.core_utils.ext.dimen
|
||||
import com.anytypeio.anytype.presentation.relations.model.LimitObjectTypeValueView
|
||||
import com.anytypeio.anytype.presentation.relations.model.RelationItemView
|
||||
import com.anytypeio.anytype.presentation.relations.model.RelationView
|
||||
import com.anytypeio.anytype.presentation.relations.model.Section
|
||||
|
||||
class RelationAddAdapter(
|
||||
val onItemClick: (RelationView.Existing) -> Unit
|
||||
) : ListAdapter<RelationView.Existing, DefaultRelationViewHolder>(Differ) {
|
||||
) : ListAdapter<RelationItemView, RecyclerView.ViewHolder>(Differ) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
) = DefaultRelationViewHolder(
|
||||
binding = ItemRelationFormatBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
itemView.setOnClickListener {
|
||||
if (bindingAdapterPosition != RecyclerView.NO_POSITION)
|
||||
onItemClick(getItem(bindingAdapterPosition))
|
||||
) = when(viewType) {
|
||||
R.layout.item_relation_format -> {
|
||||
DefaultRelationViewHolder(
|
||||
binding = ItemRelationFormatBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
itemView.setOnClickListener {
|
||||
if (bindingAdapterPosition != RecyclerView.NO_POSITION) {
|
||||
val item = getItem(bindingAdapterPosition)
|
||||
if (item is RelationView.Existing) { onItemClick(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
R.layout.item_default_list_section -> {
|
||||
DefaultSectionViewHolder(
|
||||
binding =ItemDefaultListSectionBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
binding.tvSectionName.updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
marginStart = dimen(R.dimen.dp_20)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected view type: $viewType")
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
when(holder) {
|
||||
is DefaultRelationViewHolder -> {
|
||||
check(item is RelationView.Existing)
|
||||
holder.bind(name = item.name, format = item.format)
|
||||
}
|
||||
is DefaultSectionViewHolder -> {
|
||||
check(item is Section)
|
||||
when(item) {
|
||||
Section.Marketplace -> {
|
||||
holder.bind(holder.itemView.resources.getString(R.string.marketplace))
|
||||
}
|
||||
Section.Library -> {
|
||||
holder.bind(holder.itemView.resources.getString(R.string.my_relations))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DefaultRelationViewHolder, position: Int) {
|
||||
getItem(position).apply { holder.bind(name = name, format = format) }
|
||||
override fun getItemViewType(position: Int): Int = when(val item = getItem(position)) {
|
||||
is RelationView.Existing -> R.layout.item_relation_format
|
||||
is Section -> R.layout.item_default_list_section
|
||||
else -> throw IllegalStateException("Unexpected type: ${item::class.simpleName}")
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_relation_format
|
||||
|
||||
object Differ : DiffUtil.ItemCallback<RelationView.Existing>() {
|
||||
object Differ : DiffUtil.ItemCallback<RelationItemView>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: RelationView.Existing,
|
||||
newItem: RelationView.Existing
|
||||
): Boolean = oldItem.key == newItem.key
|
||||
oldItem: RelationItemView,
|
||||
newItem: RelationItemView
|
||||
): Boolean {
|
||||
return if (oldItem is RelationView.Existing && newItem is RelationView.Existing)
|
||||
oldItem.key == newItem.key
|
||||
else
|
||||
oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: RelationView.Existing,
|
||||
newItem: RelationView.Existing
|
||||
oldItem: RelationItemView,
|
||||
newItem: RelationItemView
|
||||
): Boolean = oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
|
|
@ -583,5 +583,6 @@
|
|||
<string name="set_by_type">Type: %1$s</string>
|
||||
<string name="marketplace">Marketplace</string>
|
||||
<string name="my_types">My types</string>
|
||||
<string name="my_relations">My relations</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package com.anytypeio.anytype.domain.relations
|
||||
|
||||
import com.anytypeio.anytype.core_models.DVFilter
|
||||
import com.anytypeio.anytype.core_models.DVSort
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
|
||||
class GetRelations(
|
||||
private val repo: BlockRepository
|
||||
) : BaseUseCase<List<ObjectWrapper.Relation>, GetRelations.Params>() {
|
||||
|
||||
override suspend fun run(params: Params) = safe { proceedWithUseCase(params) }
|
||||
|
||||
suspend fun execute(params: Params) : List<ObjectWrapper.Relation> {
|
||||
return proceedWithUseCase(params)
|
||||
}
|
||||
|
||||
private suspend fun proceedWithUseCase(params: Params) = repo.searchObjects(
|
||||
keys = params.keys,
|
||||
filters = params.filters,
|
||||
sorts = params.sorts,
|
||||
limit = params.limit,
|
||||
offset = params.offset,
|
||||
fulltext = params.query
|
||||
).map { struct ->
|
||||
ObjectWrapper.Relation(struct)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val sorts: List<DVSort> = emptyList(),
|
||||
val filters: List<DVFilter> = emptyList(),
|
||||
val keys: List<Key> = emptyList(),
|
||||
val offset: Int = 0,
|
||||
val limit: Int = 0,
|
||||
val query: String = ""
|
||||
)
|
||||
}
|
|
@ -178,7 +178,7 @@ class ObjectTypeChangeViewModel(
|
|||
)
|
||||
}
|
||||
},
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
query = query,
|
||||
keys = ObjectSearchConstants.defaultKeysObjectType
|
||||
)
|
||||
|
@ -203,7 +203,7 @@ class ObjectTypeChangeViewModel(
|
|||
)
|
||||
}
|
||||
},
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
query = query,
|
||||
keys = ObjectSearchConstants.defaultKeysObjectType
|
||||
)
|
||||
|
|
|
@ -7,13 +7,16 @@ import com.anytypeio.anytype.analytics.base.Analytics
|
|||
import com.anytypeio.anytype.core_models.DV
|
||||
import com.anytypeio.anytype.core_models.DVViewerRelation
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.dataview.interactor.AddRelationToDataView
|
||||
import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfRelations
|
||||
import com.anytypeio.anytype.domain.relations.GetRelations
|
||||
import com.anytypeio.anytype.domain.workspace.AddObjectToWorkspace
|
||||
import com.anytypeio.anytype.presentation.extension.getPropName
|
||||
import com.anytypeio.anytype.presentation.extension.sendAnalyticsAddRelationEvent
|
||||
import com.anytypeio.anytype.presentation.relations.model.RelationView
|
||||
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
|
||||
import com.anytypeio.anytype.presentation.sets.ObjectSet
|
||||
import com.anytypeio.anytype.presentation.sets.ObjectSetSession
|
||||
|
@ -23,25 +26,35 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
|
||||
class RelationAddToDataViewViewModel(
|
||||
storeOfRelations: StoreOfRelations,
|
||||
relationsProvider: ObjectRelationProvider,
|
||||
private val state: StateFlow<ObjectSet>,
|
||||
private val session: ObjectSetSession,
|
||||
private val updateDataViewViewer: UpdateDataViewViewer,
|
||||
private val addRelationToDataView: AddRelationToDataView,
|
||||
private val getRelations: GetRelations,
|
||||
private val dispatcher: Dispatcher<Payload>,
|
||||
private val analytics: Analytics
|
||||
private val analytics: Analytics,
|
||||
private val addObjectToWorkspace: AddObjectToWorkspace,
|
||||
appCoroutineDispatchers: AppCoroutineDispatchers
|
||||
) : RelationAddViewModelBase(
|
||||
storeOfRelations = storeOfRelations,
|
||||
relationsProvider = relationsProvider
|
||||
relationsProvider = relationsProvider,
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
getRelations = getRelations,
|
||||
addObjectToWorkspace = addObjectToWorkspace
|
||||
) {
|
||||
|
||||
fun onRelationSelected(ctx: Id, relation: RelationView.Existing, dv: Id, screenType: String) {
|
||||
fun onRelationSelected(
|
||||
ctx: Id,
|
||||
relation: Key,
|
||||
format: RelationFormat,
|
||||
dv: Id,
|
||||
screenType: String
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
addRelationToDataView(
|
||||
AddRelationToDataView.Params(
|
||||
ctx = ctx,
|
||||
relation = relation.key,
|
||||
relation = relation,
|
||||
dv = dv
|
||||
)
|
||||
).process(
|
||||
|
@ -49,13 +62,13 @@ class RelationAddToDataViewViewModel(
|
|||
dispatcher.send(it).also {
|
||||
proceedWithAddingNewRelationToCurrentViewer(
|
||||
ctx = ctx,
|
||||
relation = relation.key
|
||||
relation = relation
|
||||
)
|
||||
}
|
||||
sendAnalyticsAddRelationEvent(
|
||||
analytics = analytics,
|
||||
type = screenType,
|
||||
format = relation.format.getPropName()
|
||||
format = format.getPropName()
|
||||
)
|
||||
},
|
||||
failure = {
|
||||
|
@ -97,23 +110,27 @@ class RelationAddToDataViewViewModel(
|
|||
private val state: StateFlow<ObjectSet>,
|
||||
private val session: ObjectSetSession,
|
||||
private val updateDataViewViewer: UpdateDataViewViewer,
|
||||
private val storeOfRelations: StoreOfRelations,
|
||||
private val addRelationToDataView: AddRelationToDataView,
|
||||
private val dispatcher: Dispatcher<Payload>,
|
||||
private val analytics: Analytics,
|
||||
private val relationsProvider: ObjectRelationProvider,
|
||||
private val appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
private val getRelations: GetRelations,
|
||||
private val addObjectToWorkspace: AddObjectToWorkspace
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return RelationAddToDataViewViewModel(
|
||||
addRelationToDataView = addRelationToDataView,
|
||||
storeOfRelations = storeOfRelations,
|
||||
dispatcher = dispatcher,
|
||||
session = session,
|
||||
updateDataViewViewer = updateDataViewViewer,
|
||||
state = state,
|
||||
analytics = analytics,
|
||||
relationsProvider = relationsProvider,
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
getRelations = getRelations,
|
||||
addObjectToWorkspace = addObjectToWorkspace
|
||||
) as T
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,17 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
import com.anytypeio.anytype.analytics.base.EventsDictionary
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfRelations
|
||||
import com.anytypeio.anytype.domain.relations.AddRelationToObject
|
||||
import com.anytypeio.anytype.domain.relations.GetRelations
|
||||
import com.anytypeio.anytype.domain.workspace.AddObjectToWorkspace
|
||||
import com.anytypeio.anytype.presentation.extension.getPropName
|
||||
import com.anytypeio.anytype.presentation.extension.sendAnalyticsAddRelationEvent
|
||||
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSearchQueryEvent
|
||||
import com.anytypeio.anytype.presentation.relations.model.RelationView
|
||||
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
|
||||
import com.anytypeio.anytype.presentation.util.Dispatcher
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
@ -24,29 +28,39 @@ class RelationAddToObjectViewModel(
|
|||
private val addRelationToObject: AddRelationToObject,
|
||||
private val dispatcher: Dispatcher<Payload>,
|
||||
private val analytics: Analytics,
|
||||
val storeOfRelations: StoreOfRelations
|
||||
val storeOfRelations: StoreOfRelations,
|
||||
appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
getRelations: GetRelations,
|
||||
addObjectToWorkspace: AddObjectToWorkspace
|
||||
) : RelationAddViewModelBase(
|
||||
relationsProvider = relationsProvider,
|
||||
storeOfRelations = storeOfRelations
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
getRelations = getRelations,
|
||||
addObjectToWorkspace = addObjectToWorkspace
|
||||
) {
|
||||
|
||||
val commands = MutableSharedFlow<Command>(replay = 0)
|
||||
|
||||
fun onRelationSelected(ctx: Id, relation: RelationView.Existing, screenType: String) {
|
||||
fun onRelationSelected(
|
||||
ctx: Id,
|
||||
relation: Key,
|
||||
format: RelationFormat,
|
||||
screenType: String
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
addRelationToObject(
|
||||
AddRelationToObject.Params(
|
||||
ctx = ctx,
|
||||
relationKey = relation.key
|
||||
relationKey = relation
|
||||
)
|
||||
).process(
|
||||
success = {
|
||||
dispatcher.send(it).also {
|
||||
commands.emit(Command.OnRelationAdd(relation = relation.key))
|
||||
commands.emit(Command.OnRelationAdd(relation = relation))
|
||||
sendAnalyticsAddRelationEvent(
|
||||
analytics = analytics,
|
||||
type = screenType,
|
||||
format = relation.format.getPropName()
|
||||
format = format.getPropName()
|
||||
)
|
||||
isDismissed.value = true
|
||||
}
|
||||
|
@ -73,6 +87,9 @@ class RelationAddToObjectViewModel(
|
|||
private val dispatcher: Dispatcher<Payload>,
|
||||
private val analytics: Analytics,
|
||||
private val relationsProvider: ObjectRelationProvider,
|
||||
private val appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
private val getRelations: GetRelations,
|
||||
private val addObjectToWorkspace: AddObjectToWorkspace
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
|
@ -81,7 +98,10 @@ class RelationAddToObjectViewModel(
|
|||
addRelationToObject = addRelationToObject,
|
||||
storeOfRelations = storeOfRelations,
|
||||
dispatcher = dispatcher,
|
||||
analytics = analytics
|
||||
analytics = analytics,
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
getRelations = getRelations,
|
||||
addObjectToWorkspace = addObjectToWorkspace
|
||||
) as T
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,84 +1,230 @@
|
|||
package com.anytypeio.anytype.presentation.relations
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.anytypeio.anytype.core_models.DVFilter
|
||||
import com.anytypeio.anytype.core_models.DVFilterCondition
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.Marketplace
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_utils.ext.cancel
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfRelations
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.relations.GetRelations
|
||||
import com.anytypeio.anytype.domain.workspace.AddObjectToWorkspace
|
||||
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
||||
import com.anytypeio.anytype.presentation.relations.model.RelationItemView
|
||||
import com.anytypeio.anytype.presentation.relations.model.RelationView
|
||||
import com.anytypeio.anytype.presentation.relations.model.Section
|
||||
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultObjectSearchSorts
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.filterMarketplaceRelations
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.filterMyRelations
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Base view model for adding a relation either to an object or to a set.
|
||||
*/
|
||||
abstract class RelationAddViewModelBase(
|
||||
private val relationsProvider: ObjectRelationProvider,
|
||||
private val storeOfRelations: StoreOfRelations
|
||||
private val getRelations: GetRelations,
|
||||
private val appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
private val addObjectToWorkspace: AddObjectToWorkspace
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val jobs = mutableListOf<Job>()
|
||||
private val userInput = MutableStateFlow(DEFAULT_INPUT)
|
||||
private val views = MutableStateFlow<List<RelationView.Existing>>(emptyList())
|
||||
|
||||
private val searchQuery = userInput.take(1).onCompletion {
|
||||
emitAll(userInput.drop(1).debounce(DEBOUNCE_DURATION).distinctUntilChanged())
|
||||
}
|
||||
|
||||
val command = MutableSharedFlow<Command>()
|
||||
val isDismissed = MutableStateFlow(false)
|
||||
val results: Flow<List<RelationView.Existing>> = combine(searchQuery, views) { query, views ->
|
||||
if (query.isEmpty())
|
||||
views
|
||||
else
|
||||
views.filter { view -> view.name.contains(query, true) }
|
||||
val results = MutableStateFlow(emptyList<RelationItemView>())
|
||||
|
||||
private val objectRelationKeys = relationsProvider.observeAll().map { relations ->
|
||||
relations.map { r -> r.key }
|
||||
}
|
||||
|
||||
abstract fun sendAnalyticsEvent(length: Int)
|
||||
|
||||
fun onStart() {
|
||||
jobs += viewModelScope.launch {
|
||||
val all = storeOfRelations.getAll().filter { it.isValid }
|
||||
getVisibleRelations(available = all).collect { filtered ->
|
||||
views.value = filtered
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
searchQuery,
|
||||
objectRelationKeys
|
||||
) { query, keys ->
|
||||
val myRelations = proceedWithGettingMyRelations(query = query)
|
||||
val marketplaceRelations = proceedWithGettingMarketplaceRelations(
|
||||
query = query,
|
||||
excluded = myRelations.mapNotNull { it.sourceObject }
|
||||
)
|
||||
buildViews(
|
||||
myRelations = myRelations,
|
||||
marketplaceRelations = marketplaceRelations,
|
||||
objectRelationKeys = keys
|
||||
)
|
||||
}.flowOn(appCoroutineDispatchers.io).catch {
|
||||
sendToast("An error occured. Please try again later.")
|
||||
}.collect { views ->
|
||||
results.value = views
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
jobs.cancel()
|
||||
}
|
||||
|
||||
private fun getVisibleRelations(available: List<ObjectWrapper.Relation>): Flow<List<RelationView.Existing>> {
|
||||
return relationsProvider.observeAll().map { addedRelations ->
|
||||
val addedRelationKeys = addedRelations.map { it.key }.toSet()
|
||||
available
|
||||
.filter { it.isHidden != true }
|
||||
.filter { relation -> !addedRelationKeys.contains(relation.key) }
|
||||
.map {
|
||||
RelationView.Existing(
|
||||
key = it.key,
|
||||
name = it.name.orEmpty(),
|
||||
format = it.format
|
||||
)
|
||||
}
|
||||
private fun buildViews(
|
||||
myRelations: List<ObjectWrapper.Relation>,
|
||||
marketplaceRelations: List<ObjectWrapper.Relation>,
|
||||
objectRelationKeys: List<Key>,
|
||||
) = buildList {
|
||||
val my = myRelations.filter {
|
||||
!objectRelationKeys.contains(it.key)
|
||||
}.map { wrapper ->
|
||||
RelationView.Existing(
|
||||
id = wrapper.id,
|
||||
key = wrapper.key,
|
||||
name = wrapper.name.orEmpty(),
|
||||
format = wrapper.format,
|
||||
workspace = wrapper.workspaceId
|
||||
)
|
||||
}
|
||||
val marketplace = marketplaceRelations.filter {
|
||||
!objectRelationKeys.contains(it.key)
|
||||
}.map { wrapper ->
|
||||
RelationView.Existing(
|
||||
id = wrapper.id,
|
||||
key = wrapper.key,
|
||||
name = wrapper.name.orEmpty(),
|
||||
format = wrapper.format,
|
||||
workspace = wrapper.workspaceId
|
||||
)
|
||||
}
|
||||
if (my.isNotEmpty()) {
|
||||
add(Section.Library)
|
||||
addAll(my)
|
||||
}
|
||||
if (marketplace.isNotEmpty()) {
|
||||
add(Section.Marketplace)
|
||||
addAll(marketplace)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithGettingMarketplaceRelations(
|
||||
excluded: List<Id>,
|
||||
query: String
|
||||
) = getRelations.execute(
|
||||
GetRelations.Params(
|
||||
sorts = defaultObjectSearchSorts(),
|
||||
filters = buildList {
|
||||
addAll(filterMarketplaceRelations())
|
||||
if (excluded.isNotEmpty()) {
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.ID,
|
||||
condition = DVFilterCondition.NOT_IN,
|
||||
value = excluded
|
||||
)
|
||||
)
|
||||
}
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_HIDDEN,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
)
|
||||
)
|
||||
},
|
||||
query = query
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun proceedWithGettingMyRelations(query: String) = getRelations.execute(
|
||||
GetRelations.Params(
|
||||
sorts = defaultObjectSearchSorts(),
|
||||
filters = buildList {
|
||||
addAll(filterMyRelations())
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_HIDDEN,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
)
|
||||
)
|
||||
},
|
||||
query = query
|
||||
)
|
||||
)
|
||||
|
||||
abstract fun sendAnalyticsEvent(length: Int)
|
||||
|
||||
fun onQueryChanged(input: String) {
|
||||
sendAnalyticsEvent(input.length)
|
||||
userInput.value = input
|
||||
}
|
||||
|
||||
fun onRelationSelected(
|
||||
ctx: Id,
|
||||
relation: RelationView.Existing
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
if (relation.workspace == Marketplace.MARKETPLACE_ID) {
|
||||
addObjectToWorkspace(
|
||||
AddObjectToWorkspace.Params(
|
||||
objects = listOf(relation.id)
|
||||
)
|
||||
).proceed(
|
||||
success = {
|
||||
sendToast("Relation `${relation.name}` added to your library")
|
||||
proceedWithDispatchingSelectedRelation(
|
||||
ctx = ctx,
|
||||
relation = relation
|
||||
)
|
||||
},
|
||||
failure = {
|
||||
Timber.e(it, "Error while adding relation to workspace.")
|
||||
sendToast("Something went wrong. Please, try again later.")
|
||||
}
|
||||
)
|
||||
}
|
||||
proceedWithDispatchingSelectedRelation(
|
||||
ctx = ctx,
|
||||
relation = relation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithDispatchingSelectedRelation(
|
||||
ctx: Id,
|
||||
relation: RelationView.Existing
|
||||
) {
|
||||
command.emit(
|
||||
Command.DispatchSelectedRelation(
|
||||
ctx = ctx,
|
||||
relation = relation.key,
|
||||
format = relation.format
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Command {
|
||||
data class DispatchSelectedRelation(
|
||||
val ctx: Id,
|
||||
val relation: Key,
|
||||
val format: RelationFormat
|
||||
): Command()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ERROR_MESSAGE = "Error while adding relation to object"
|
||||
const val DEBOUNCE_DURATION = 300L
|
||||
|
|
|
@ -1,20 +1,31 @@
|
|||
package com.anytypeio.anytype.presentation.relations.model
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
|
||||
sealed class RelationView {
|
||||
|
||||
sealed class RelationItemView
|
||||
|
||||
sealed class RelationView : RelationItemView() {
|
||||
|
||||
abstract val format: RelationFormat
|
||||
|
||||
data class Existing(
|
||||
val id: Id,
|
||||
val key: Key,
|
||||
val workspace: Id?,
|
||||
val name: String,
|
||||
override val format: RelationFormat
|
||||
override val format: RelationFormat,
|
||||
) : RelationView()
|
||||
|
||||
data class CreateFromScratch(
|
||||
override val format: RelationFormat,
|
||||
val isSelected: Boolean = false
|
||||
) : RelationView()
|
||||
}
|
||||
|
||||
sealed class Section : RelationItemView() {
|
||||
object Library: Section()
|
||||
object Marketplace : Section()
|
||||
}
|
|
@ -401,10 +401,46 @@ object ObjectSearchConstants {
|
|||
|
||||
//endregion
|
||||
|
||||
fun defaultObjectTypeSorts() : List<DVSort> = listOf(
|
||||
fun defaultObjectSearchSorts() : List<DVSort> = listOf(
|
||||
DVSort(
|
||||
relationKey = Relations.NAME,
|
||||
type = DVSortType.ASC
|
||||
)
|
||||
)
|
||||
|
||||
fun filterMyRelations() : List<DVFilter> = listOf(
|
||||
DVFilter(
|
||||
relationKey = Relations.TYPE,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = RELATION
|
||||
),
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_ARCHIVED,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
),
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_DELETED,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
)
|
||||
)
|
||||
|
||||
fun filterMarketplaceRelations() : List<DVFilter> = listOf(
|
||||
DVFilter(
|
||||
relationKey = Relations.TYPE,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = MarketplaceObjectTypeIds.RELATION
|
||||
),
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_ARCHIVED,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
),
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_DELETED,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
)
|
||||
)
|
||||
}
|
|
@ -1,81 +1,275 @@
|
|||
package com.anytypeio.anytype.presentation.relations
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.anytypeio.anytype.core_models.DVFilter
|
||||
import com.anytypeio.anytype.core_models.DVFilterCondition
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.StubRelationObject
|
||||
import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfRelations
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.relations.GetRelations
|
||||
import com.anytypeio.anytype.domain.workspace.AddObjectToWorkspace
|
||||
import com.anytypeio.anytype.presentation.relations.model.RelationView
|
||||
import com.anytypeio.anytype.presentation.relations.model.Section
|
||||
import com.anytypeio.anytype.presentation.relations.providers.FakeObjectRelationProvider
|
||||
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
|
||||
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.stub
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class RelationAddViewModelBaseTest {
|
||||
|
||||
@get:Rule
|
||||
internal val coroutineTestRule = CoroutinesTestRule()
|
||||
val coroutineTestRule = CoroutinesTestRule()
|
||||
|
||||
private val availableHidden = StubRelationObject(isHidden = true)
|
||||
private val available = StubRelationObject()
|
||||
private val availableRelations = listOf(available, availableHidden)
|
||||
private val appCoroutineDispatchers = AppCoroutineDispatchers(
|
||||
io = coroutineTestRule.testDispatcher,
|
||||
main = coroutineTestRule.testDispatcher,
|
||||
computation = coroutineTestRule.testDispatcher
|
||||
)
|
||||
|
||||
@Mock
|
||||
lateinit var repo: BlockRepository
|
||||
|
||||
private val relationsProvider = FakeObjectRelationProvider()
|
||||
|
||||
@Test
|
||||
fun `no added relations - results are available without hidden`() {
|
||||
runTest {
|
||||
// SETUP
|
||||
val store = DefaultStoreOfRelations()
|
||||
val vm = createVM(store)
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.openMocks(this)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
store.merge(availableRelations)
|
||||
vm.onStart()
|
||||
coroutineTestRule.testDispatcher.scheduler.runCurrent()
|
||||
vm.results.test {
|
||||
assertEquals(
|
||||
actual = awaitItem(),
|
||||
expected = listOf(
|
||||
RelationView.Existing(
|
||||
key = available.key,
|
||||
name = available.name.orEmpty(),
|
||||
format = available.format
|
||||
@Test
|
||||
fun `no added relations - results are available without hidden`() = runTest {
|
||||
|
||||
// SETUP
|
||||
|
||||
val relation = StubRelationObject()
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
searchObjects(
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
filters = buildList {
|
||||
addAll(ObjectSearchConstants.filterMyRelations())
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_HIDDEN,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
)
|
||||
)
|
||||
},
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = ""
|
||||
)
|
||||
} doReturn listOf(relation.map)
|
||||
}
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
searchObjects(
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
filters = buildList {
|
||||
addAll(ObjectSearchConstants.filterMarketplaceRelations())
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.ID,
|
||||
condition = DVFilterCondition.NOT_IN,
|
||||
value = listOf(relation.sourceObject)
|
||||
)
|
||||
)
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_HIDDEN,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
)
|
||||
)
|
||||
},
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = ""
|
||||
)
|
||||
} doReturn emptyList()
|
||||
}
|
||||
|
||||
val vm = givenViewModel(relationsProvider = relationsProvider)
|
||||
|
||||
// TESTING
|
||||
|
||||
coroutineTestRule.testDispatcher.scheduler.runCurrent()
|
||||
|
||||
vm.results.test {
|
||||
assertEquals(
|
||||
actual = awaitItem(),
|
||||
expected = listOf(
|
||||
Section.Library,
|
||||
RelationView.Existing(
|
||||
key = relation.key,
|
||||
id = relation.id,
|
||||
name = relation.name.orEmpty(),
|
||||
format = relation.format,
|
||||
workspace = null
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `added relations equal to available - results are empty`() {
|
||||
relationsProvider.relation = available
|
||||
runTest {
|
||||
fun `added relations equal to available - results are empty`() = runTest {
|
||||
// SETUP
|
||||
val store = DefaultStoreOfRelations()
|
||||
val vm = createVM(store)
|
||||
val vm = givenViewModel(relationsProvider)
|
||||
|
||||
// TESTING
|
||||
store.merge(availableRelations)
|
||||
vm.onStart()
|
||||
|
||||
vm.results.test {
|
||||
assertEquals(
|
||||
actual = awaitItem(),
|
||||
expected = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should query relations from library and marketplace filtering out already addded relations`() = runTest {
|
||||
|
||||
// SETUP
|
||||
|
||||
val marketplace = listOf(
|
||||
StubRelationObject(),
|
||||
StubRelationObject(),
|
||||
StubRelationObject()
|
||||
)
|
||||
|
||||
val library = listOf(
|
||||
StubRelationObject(sourceObject = marketplace[0].id),
|
||||
StubRelationObject(),
|
||||
StubRelationObject()
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
searchObjects(
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
filters = buildList {
|
||||
addAll(ObjectSearchConstants.filterMyRelations())
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_HIDDEN,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
)
|
||||
)
|
||||
},
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = ""
|
||||
)
|
||||
} doReturn library.map { it.map }
|
||||
}
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
searchObjects(
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
filters = buildList {
|
||||
addAll(ObjectSearchConstants.filterMarketplaceRelations())
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.ID,
|
||||
condition = DVFilterCondition.NOT_IN,
|
||||
value = library.mapNotNull { it.sourceObject }
|
||||
)
|
||||
)
|
||||
add(
|
||||
DVFilter(
|
||||
relationKey = Relations.IS_HIDDEN,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = false
|
||||
)
|
||||
)
|
||||
},
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = ""
|
||||
)
|
||||
} doReturn marketplace.takeLast(2).map { it.map }
|
||||
}
|
||||
|
||||
val vm = givenViewModel(relationsProvider = relationsProvider)
|
||||
|
||||
// TESTING
|
||||
|
||||
coroutineTestRule.testDispatcher.scheduler.runCurrent()
|
||||
|
||||
vm.results.test {
|
||||
assertEquals(
|
||||
actual = awaitItem(),
|
||||
expected = listOf(
|
||||
Section.Library,
|
||||
RelationView.Existing(
|
||||
key = library[0].key,
|
||||
id = library[0].id,
|
||||
name = library[0].name.orEmpty(),
|
||||
format = library[0].format,
|
||||
workspace = null
|
||||
),
|
||||
RelationView.Existing(
|
||||
key = library[1].key,
|
||||
id = library[1].id,
|
||||
name = library[1].name.orEmpty(),
|
||||
format = library[1].format,
|
||||
workspace = null
|
||||
),
|
||||
RelationView.Existing(
|
||||
key = library[2].key,
|
||||
id = library[2].id,
|
||||
name = library[2].name.orEmpty(),
|
||||
format = library[2].format,
|
||||
workspace = null
|
||||
),
|
||||
Section.Marketplace,
|
||||
RelationView.Existing(
|
||||
key = marketplace[1].key,
|
||||
id = marketplace[1].id,
|
||||
name = marketplace[1].name.orEmpty(),
|
||||
format = marketplace[1].format,
|
||||
workspace = null
|
||||
),
|
||||
RelationView.Existing(
|
||||
key = marketplace[2].key,
|
||||
id = marketplace[2].id,
|
||||
name = marketplace[2].name.orEmpty(),
|
||||
format = marketplace[2].format,
|
||||
workspace = null
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createVM(
|
||||
storeOfRelations: StoreOfRelations
|
||||
private fun givenViewModel(
|
||||
relationsProvider: ObjectRelationProvider
|
||||
) = object : RelationAddViewModelBase(
|
||||
storeOfRelations = storeOfRelations,
|
||||
relationsProvider = relationsProvider
|
||||
relationsProvider = relationsProvider,
|
||||
getRelations = GetRelations(repo),
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
addObjectToWorkspace = AddObjectToWorkspace(
|
||||
repo = repo,
|
||||
dispatchers = appCoroutineDispatchers
|
||||
)
|
||||
) {
|
||||
override fun sendAnalyticsEvent(length: Int) {}
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = ObjectSearchConstants.defaultKeysObjectType,
|
||||
|
@ -143,7 +143,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
onBlocking {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = "",
|
||||
|
@ -171,7 +171,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = expectedMyTypeKeys,
|
||||
|
@ -184,7 +184,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMarketplaceTypeFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = expectedMarketplaceTypeKeys,
|
||||
|
@ -230,7 +230,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
onBlocking {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = "",
|
||||
|
@ -245,7 +245,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
onBlocking {
|
||||
searchObjects(
|
||||
filters = expectedMarketplaceTypeFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = "",
|
||||
|
@ -258,7 +258,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
onBlocking {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = query,
|
||||
|
@ -273,7 +273,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
onBlocking {
|
||||
searchObjects(
|
||||
filters = expectedMarketplaceTypeFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = query,
|
||||
|
@ -299,7 +299,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = expectedMyTypeKeys,
|
||||
|
@ -312,7 +312,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMarketplaceTypeFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = expectedMarketplaceTypeKeys,
|
||||
|
@ -331,7 +331,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = expectedMyTypeKeys,
|
||||
|
@ -344,7 +344,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMarketplaceTypeFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = expectedMarketplaceTypeKeys,
|
||||
|
@ -389,7 +389,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
onBlocking {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = "",
|
||||
|
@ -404,7 +404,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
onBlocking {
|
||||
searchObjects(
|
||||
filters = expectedMarketplaceTypeFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = "",
|
||||
|
@ -436,7 +436,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = expectedMyTypeKeys,
|
||||
|
@ -449,7 +449,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
verifyBlocking(repo, times(1)) {
|
||||
searchObjects(
|
||||
filters = expectedMarketplaceTypeFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
keys = expectedMarketplaceTypeKeys,
|
||||
|
@ -496,7 +496,7 @@ class ObjectTypeChangeViewModelTest {
|
|||
onBlocking {
|
||||
searchObjects(
|
||||
filters = expectedMyTypesFilters,
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSorts(),
|
||||
sorts = ObjectSearchConstants.defaultObjectSearchSorts(),
|
||||
limit = 0,
|
||||
offset = 0,
|
||||
fulltext = "",
|
||||
|
|
|
@ -2,31 +2,6 @@ package com.anytypeio.anytype.core_models
|
|||
|
||||
import com.anytypeio.anytype.test_utils.MockDataFactory
|
||||
|
||||
@Deprecated("To be deleted")
|
||||
fun StubRelation(
|
||||
key: String = MockDataFactory.randomString(),
|
||||
name: String = MockDataFactory.randomString(),
|
||||
format: Relation.Format = Relation.Format.SHORT_TEXT,
|
||||
source: Relation.Source = Relation.Source.LOCAL,
|
||||
isHidden: Boolean = false,
|
||||
isReadOnly: Boolean = false,
|
||||
isMulti: Boolean = false,
|
||||
selections: List<Relation.Option> = emptyList(),
|
||||
objectTypes: List<String> = emptyList(),
|
||||
defaultValue: Any? = null
|
||||
): Relation = Relation(
|
||||
key,
|
||||
name,
|
||||
format,
|
||||
source,
|
||||
isHidden,
|
||||
isReadOnly,
|
||||
isMulti,
|
||||
selections,
|
||||
objectTypes,
|
||||
defaultValue
|
||||
)
|
||||
|
||||
fun StubRelationObject(
|
||||
id: String = MockDataFactory.randomString(),
|
||||
key: String = MockDataFactory.randomString(),
|
||||
|
@ -35,7 +10,8 @@ fun StubRelationObject(
|
|||
isHidden: Boolean = false,
|
||||
isReadOnly: Boolean = false,
|
||||
objectTypes: List<Id> = emptyList(),
|
||||
relationOptionsDict: List<Id> = emptyList()
|
||||
relationOptionsDict: List<Id> = emptyList(),
|
||||
sourceObject: Id = MockDataFactory.randomUuid()
|
||||
): ObjectWrapper.Relation = ObjectWrapper.Relation(
|
||||
map = mapOf(
|
||||
Relations.ID to id,
|
||||
|
@ -45,21 +21,11 @@ fun StubRelationObject(
|
|||
Relations.IS_READ_ONLY to isReadOnly,
|
||||
Relations.RELATION_FORMAT_OBJECT_TYPES to objectTypes,
|
||||
Relations.RELATION_FORMAT to format.code.toDouble(),
|
||||
Relations.RELATION_OPTION_DICT to relationOptionsDict
|
||||
Relations.RELATION_OPTION_DICT to relationOptionsDict,
|
||||
Relations.SOURCE_OBJECT to sourceObject
|
||||
)
|
||||
)
|
||||
|
||||
@Deprecated("To be deleted")
|
||||
fun StubRelationOption(
|
||||
id: String = MockDataFactory.randomUuid(),
|
||||
text: String = MockDataFactory.randomString(),
|
||||
color: String = MockDataFactory.randomString()
|
||||
): Relation.Option = Relation.Option(
|
||||
id = id,
|
||||
text = text,
|
||||
color = color
|
||||
)
|
||||
|
||||
fun StubRelationOptionObject(
|
||||
id: String = MockDataFactory.randomUuid(),
|
||||
text: String = MockDataFactory.randomString(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue