1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Relations | Fix | Filter create option for tags that already exist (#2237)

* Relations | Fix | Filter create option for tags that already exist

* Tech | Fix | Change compile android tests task

* Tech | Fix | add build folders ignore to global .gitignore

* Tech | Fix | Extract MockDataFactory.kt from test-utils

* Tech | Fix | Move return to the same line as checkNotNull

* Tech | Fix | Delete unused analytics property

* Relations | Fix | Add AddObjectRelationValueViewModelTest.kt test query with tags behaviour

* Tech | Fix | Rename :test:utils to :test:android-utils

* Tech | Enhancement | implement fake providers for AddObjectRelationViewModerl
This commit is contained in:
Sergey Boishtyan 2022-05-09 17:54:15 +03:00 committed by GitHub
parent 8e678c95d4
commit 58da3badfe
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 770 additions and 469 deletions

View file

@ -18,7 +18,7 @@ jobs:
amplitude_secret_debug: ${{ secrets.ANYTYPE_AMPLITUDE_DEBUG_SECRET }}
run: ./middleware2.sh $token_secret $user_secret $amplitude_secret $amplitude_secret_debug
- name: Compile android test sources
run: ./gradlew compileDebugAndroidTestSources
run: ./gradlew compileExperimentalDebugAndroidTestSources
- name: Run unit tests
run: ./gradlew testDebugUnitTest -Dpre-dex=false
- name: Android test report

2
.gitignore vendored
View file

@ -8,7 +8,7 @@
/github.properties
/apikeys.properties
.DS_Store
/build
**/build
/libs
/captures
.externalNativeBuild

View file

@ -221,7 +221,7 @@ dependencies {
testImplementation unitTestDependencies.mockitoKotlin
//Acceptance tests dependencies
androidTestImplementation project(':test-utils')
androidTestImplementation project(':test:android-utils')
androidTestImplementation acceptanceTesting.mockitoAndroid
androidTestImplementation unitTestDependencies.mockitoKotlin
androidTestImplementation acceptanceTesting.espressoCore

View file

@ -117,7 +117,6 @@ class AddRelationStatusValueTest {
addStatusToDataViewRecord = addStatusToDataViewRecord,
urlBuilder = urlBuilder,
dispatcher = dispatcher,
analytics = analytics
)
}

View file

@ -116,7 +116,6 @@ class AddRelationTagValueTest {
addStatusToDataViewRecord = addStatusToDataViewRecord,
urlBuilder = urlBuilder,
dispatcher = dispatcher,
analytics = analytics
)
}

View file

@ -52,7 +52,6 @@ object AddObjectRelationValueModule {
addTagToDataViewRecord: AddTagToDataViewRecord,
addStatusToDataViewRecord: AddStatusToDataViewRecord,
urlBuilder: UrlBuilder,
analytics: Analytics
): RelationOptionValueDVAddViewModel.Factory = RelationOptionValueDVAddViewModel.Factory(
relations = relations,
values = values,
@ -63,7 +62,6 @@ object AddObjectRelationValueModule {
addDataViewRelationOption = addDataViewRelationOption,
addTagToDataViewRecord = addTagToDataViewRecord,
addStatusToDataViewRecord = addStatusToDataViewRecord,
analytics = analytics
)
@JvmStatic

View file

@ -1 +0,0 @@
/build

View file

@ -1,13 +1,11 @@
apply plugin: 'kotlin'
apply plugin: 'org.jetbrains.dokka'
plugins {
id "kotlin"
id "org.jetbrains.dokka"
}
dependencies {
implementation mainApplication.kotlin
def applicationDependencies = rootProject.ext.mainApplication
def unitTestDependencies = rootProject.ext.unitTesting
implementation applicationDependencies.kotlin
testImplementation unitTestDependencies.kotlinTest
testImplementation unitTestDependencies.mockitoKotlin
testImplementation unitTesting.kotlinTest
testImplementation unitTesting.mockitoKotlin
}

View file

@ -76,7 +76,7 @@ dependencies {
debugImplementation applicationDependencies.composeTooling
testImplementation acceptanceTesting.fragmentTesting
testImplementation project(':test-utils')
testImplementation project(':test:android-utils')
testImplementation acceptanceTesting.espressoCore
testImplementation unitTestDependencies.junit
testImplementation unitTestDependencies.kotlinTest

View file

@ -18,8 +18,7 @@ inline fun <reified T> Fragment.argOrNull(key: String): T? {
fun Fragment.argString(key: String): String {
val value = requireArguments().getString(key)
checkNotNull(value) { "Value missing for $key" }
return value
return checkNotNull(value) { "Value missing for $key" }
}
fun Fragment.argStringOrNull(key: String): String? {
@ -36,8 +35,7 @@ fun Fragment.argLong(key: String): Long {
fun <T : Parcelable> Fragment.argList(key: String): ArrayList<T> {
val value = requireArguments().getParcelableArrayList<T>(key)
checkNotNull(value)
return value
return checkNotNull(value)
}
fun <T> CoroutineScope.subscribe(flow: Flow<T>, body: suspend (T) -> Unit): Job =

View file

@ -55,4 +55,5 @@ dependencies {
testImplementation unitTestDependencies.robolectricLatest
testImplementation unitTestDependencies.timberJUnit
testImplementation unitTestDependencies.turbine
testImplementation project(":test:core-models-stub")
}

View file

@ -1,37 +1,25 @@
package com.anytypeio.anytype.presentation.relations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
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_utils.ext.typeOf
import com.anytypeio.anytype.domain.`object`.ObjectTypesProvider
import com.anytypeio.anytype.domain.`object`.UpdateDetail
import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption
import com.anytypeio.anytype.domain.dataview.interactor.AddStatusToDataViewRecord
import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.relations.AddObjectRelationOption
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.editor.editor.ThemeColor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.getProperName
import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider
import com.anytypeio.anytype.presentation.sets.RelationValueBaseViewModel.RelationValueView
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class AddObjectRelationValueViewModel(
@ -40,7 +28,6 @@ abstract class AddObjectRelationValueViewModel(
protected val relations: ObjectRelationProvider,
protected val types: ObjectTypesProvider,
protected val urlBuilder: UrlBuilder,
analytics: Analytics
) : BaseViewModel() {
private val jobs = mutableListOf<Job>()
@ -60,34 +47,49 @@ abstract class AddObjectRelationValueViewModel(
init {
viewModelScope.launch {
views.combine(query) { all, query ->
if (query.isEmpty())
all
else
mutableListOf<RelationValueView>().apply {
add(RelationValueView.Create(query))
addAll(
all.filter { view ->
when(view) {
is RelationValueView.Status -> {
view.name.contains(query, true)
}
is RelationValueView.Tag -> {
view.name.contains(query, true)
}
else -> true
}
}
)
}
println("views=$all, query=$query")
filterRelationsBy(query, all)
}.collect { ui.value = it }
}
}
private fun filterRelationsBy(
query: String,
all: List<RelationValueView>
): List<RelationValueView> {
return if (query.isEmpty())
all
else {
val result = mutableListOf<RelationValueView>()
val filteredRelationViews = all.filter { view ->
when (view) {
is RelationValueView.Status -> {
view.name.contains(query, true)
}
is RelationValueView.Tag -> {
view.name.contains(query, true)
}
else -> true
}
}
val searchedExistentTag =
filteredRelationViews.filterIsInstance<RelationValueView.Tag>()
.any { view -> view.name == query }
if (!searchedExistentTag) {
result.add(RelationValueView.Create(query))
}
result.addAll(filteredRelationViews)
result
}
}
fun onStart(target: Id, relationId: Id) {
val s1 = relations.subscribe(relationId)
val s2 = values.subscribe(target)
jobs += viewModelScope.launch {
s1.combine(s2) { relation, record ->
println("relation=$relation, record=$record")
buildViews(relation, record, relationId).also {
if (relation.format == Relation.Format.STATUS) {
isAddButtonVisible.value = false
@ -216,9 +218,12 @@ abstract class AddObjectRelationValueViewModel(
}
views.value = result
println("views.value set $result")
}
fun onFilterInputChanged(input: String) { query.value = input }
fun onFilterInputChanged(input: String) {
query.value = input
}
fun onTagClicked(tag: RelationValueView.Tag) {
views.value = views.value.map { view ->
@ -233,372 +238,9 @@ abstract class AddObjectRelationValueViewModel(
view
}
}.also { result ->
counter.value = result.count { it is RelationValueView.Selectable && it.isSelected == true }
counter.value =
result.count { it is RelationValueView.Selectable && it.isSelected == true }
}
}
}
class RelationOptionValueDVAddViewModel(
details: ObjectDetailProvider,
types: ObjectTypesProvider,
urlBuilder: UrlBuilder,
values: ObjectValueProvider,
relations: ObjectRelationProvider,
private val addDataViewRelationOption: AddDataViewRelationOption,
private val addTagToDataViewRecord: AddTagToDataViewRecord,
private val addStatusToDataViewRecord: AddStatusToDataViewRecord,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics
) : AddObjectRelationValueViewModel(
details = details,
values = values,
types = types,
urlBuilder = urlBuilder,
relations = relations,
analytics = analytics
) {
fun onCreateDataViewRelationOptionClicked(
ctx: Id,
dataview: Id,
viewer: Id,
relation: Id,
target: Id,
name: String
) {
viewModelScope.launch {
addDataViewRelationOption(
AddDataViewRelationOption.Params(
ctx = ctx,
relation = relation,
dataview = dataview,
record = target,
name = name,
color = ThemeColor.values().filter { it != ThemeColor.DEFAULT }.random().title
)
).proceed(
success = { (payload, option) ->
dispatcher.send(payload)
if (option != null) {
when (relations.get(relation).format) {
Relation.Format.TAG -> {
proceedWithAddingTagToDataViewRecord(
ctx = ctx,
dataview = dataview,
viewer = viewer,
relation = relation,
target = target,
tags = listOf(option)
)
}
Relation.Format.STATUS -> {
proceedWithAddingStatusToDataViewRecord(
ctx = ctx,
dataview = dataview,
viewer = viewer,
relation = relation,
obj = target,
status = option
)
}
else -> Timber.e("Trying to create option for wrong relation.")
}
}
},
failure = { Timber.e(it, "Error while creating a new option") }
)
}
}
fun onAddObjectSetStatusClicked(
ctx: Id,
dataview: Id,
viewer: Id,
relation: Id,
obj: Id,
status: RelationValueView.Status
) {
proceedWithAddingStatusToDataViewRecord(ctx, dataview, viewer, relation, obj, status.id)
}
private fun proceedWithAddingStatusToDataViewRecord(
ctx: Id,
dataview: Id,
viewer: Id,
relation: Id,
obj: Id,
status: Id
) {
viewModelScope.launch {
addStatusToDataViewRecord(
AddStatusToDataViewRecord.Params(
ctx = ctx,
dataview = dataview,
viewer = viewer,
relation = relation,
obj = obj,
status = status,
record = values.get(target = obj)
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = { isParentDismissed.value = true }
)
}
}
fun onAddSelectedValuesToDataViewClicked(
ctx: Id,
dataview: Id,
target: Id,
relation: Id,
viewer: Id
) {
val tags = views.value.mapNotNull { view ->
if (view is RelationValueView.Tag && view.isSelected == true)
view.id
else
null
}
proceedWithAddingTagToDataViewRecord(
target = target,
ctx = ctx,
dataview = dataview,
relation = relation,
viewer = viewer,
tags = tags
)
}
private fun proceedWithAddingTagToDataViewRecord(
target: Id,
ctx: Id,
dataview: Id,
relation: Id,
viewer: Id,
tags: List<Id>
) {
viewModelScope.launch {
val record = values.get(target = target)
addTagToDataViewRecord(
AddTagToDataViewRecord.Params(
ctx = ctx,
tags = tags,
record = record,
dataview = dataview,
relation = relation,
viewer = viewer,
target = target
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = { isDismissed.value = true }
)
}
}
class Factory(
private val values: ObjectValueProvider,
private val details: ObjectDetailProvider,
private val relations: ObjectRelationProvider,
private val types: ObjectTypesProvider,
private val addDataViewRelationOption: AddDataViewRelationOption,
private val addTagToDataViewRecord: AddTagToDataViewRecord,
private val addStatusToDataViewRecord: AddStatusToDataViewRecord,
private val urlBuilder: UrlBuilder,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return RelationOptionValueDVAddViewModel(
details = details,
values = values,
relations = relations,
types = types,
urlBuilder = urlBuilder,
addDataViewRelationOption = addDataViewRelationOption,
addTagToDataViewRecord = addTagToDataViewRecord,
addStatusToDataViewRecord = addStatusToDataViewRecord,
dispatcher = dispatcher,
analytics = analytics
) as T
}
}
}
class RelationOptionValueAddViewModel(
details: ObjectDetailProvider,
types: ObjectTypesProvider,
urlBuilder: UrlBuilder,
values: ObjectValueProvider,
relations: ObjectRelationProvider,
private val addObjectRelationOption: AddObjectRelationOption,
private val updateDetail: UpdateDetail,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics
) : AddObjectRelationValueViewModel(
details = details,
values = values,
types = types,
urlBuilder = urlBuilder,
relations = relations,
analytics = analytics
) {
fun onAddObjectStatusClicked(
ctx: Id,
relation: Id,
status: RelationValueView.Status
) = proceedWithAddingStatusToObject(
ctx = ctx,
relation = relation,
status = status.id
)
private fun proceedWithAddingStatusToObject(
ctx: Id,
relation: Id,
status: Id
) {
viewModelScope.launch {
updateDetail(
UpdateDetail.Params(
ctx = ctx,
key = relation,
value = listOf(status)
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = { dispatcher.send(it).also {
sendAnalyticsRelationValueEvent(analytics)
isParentDismissed.value = true
}
}
)
}
}
fun onAddSelectedValuesToObjectClicked(
ctx: Id,
obj: Id,
relation: Id
) {
val tags = views.value.mapNotNull { view ->
if (view is RelationValueView.Tag && view.isSelected == true)
view.id
else
null
}
proceedWithAddingTagToObject(
obj = obj,
ctx = ctx,
relation = relation,
tags = tags
)
}
private fun proceedWithAddingTagToObject(
obj: Id,
ctx: Id,
relation: Id,
tags: List<Id>
) {
viewModelScope.launch {
val obj = values.get(target = obj)
val result = mutableListOf<Id>()
val value = obj[relation]
if (value is List<*>) {
result.addAll(value.typeOf())
} else if (value is Id) {
result.add(value)
}
result.addAll(tags)
updateDetail(
UpdateDetail.Params(
ctx = ctx,
key = relation,
value = result
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = { dispatcher.send(it).also {
sendAnalyticsRelationValueEvent(analytics)
isDismissed.value = true
} }
)
}
}
fun onCreateObjectRelationOptionClicked(
ctx: Id,
relation: Id,
name: String,
obj: Id
) {
viewModelScope.launch {
addObjectRelationOption(
AddObjectRelationOption.Params(
ctx = ctx,
relation = relation,
name = name,
color = ThemeColor.values().filter { it != ThemeColor.DEFAULT }.random().title
)
).proceed(
success = { (payload, option) ->
dispatcher.send(payload)
if (option != null) {
when (val format = relations.get(relation).format) {
Relation.Format.TAG -> {
proceedWithAddingTagToObject(
ctx = ctx,
relation = relation,
obj = obj,
tags = listOf(option)
)
}
Relation.Format.STATUS -> {
proceedWithAddingStatusToObject(
ctx = ctx,
relation = relation,
status = option
)
}
else -> {
Timber.e("Trying to create an option for relation format: $format")
}
}
}
},
failure = { Timber.e(it, "Error while creating a new option for object") }
)
}
}
class Factory(
private val values: ObjectValueProvider,
private val details: ObjectDetailProvider,
private val relations: ObjectRelationProvider,
private val types: ObjectTypesProvider,
private val addObjectRelationOption: AddObjectRelationOption,
private val updateDetail: UpdateDetail,
private val urlBuilder: UrlBuilder,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return RelationOptionValueAddViewModel(
details = details,
values = values,
relations = relations,
types = types,
urlBuilder = urlBuilder,
addObjectRelationOption = addObjectRelationOption,
updateDetail = updateDetail,
dispatcher = dispatcher,
analytics = analytics
) as T
}
}
}

View file

@ -0,0 +1,201 @@
package com.anytypeio.anytype.presentation.relations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.core_utils.ext.typeOf
import com.anytypeio.anytype.domain.`object`.ObjectTypesProvider
import com.anytypeio.anytype.domain.`object`.UpdateDetail
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.relations.AddObjectRelationOption
import com.anytypeio.anytype.presentation.editor.editor.ThemeColor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent
import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider
import com.anytypeio.anytype.presentation.sets.RelationValueBaseViewModel
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.launch
import timber.log.Timber
class RelationOptionValueAddViewModel(
details: ObjectDetailProvider,
types: ObjectTypesProvider,
urlBuilder: UrlBuilder,
values: ObjectValueProvider,
relations: ObjectRelationProvider,
private val addObjectRelationOption: AddObjectRelationOption,
private val updateDetail: UpdateDetail,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics
) : AddObjectRelationValueViewModel(
details = details,
values = values,
types = types,
urlBuilder = urlBuilder,
relations = relations,
) {
fun onAddObjectStatusClicked(
ctx: Id,
relation: Id,
status: RelationValueBaseViewModel.RelationValueView.Status
) = proceedWithAddingStatusToObject(
ctx = ctx,
relation = relation,
status = status.id
)
private fun proceedWithAddingStatusToObject(
ctx: Id,
relation: Id,
status: Id
) {
viewModelScope.launch {
updateDetail(
UpdateDetail.Params(
ctx = ctx,
key = relation,
value = listOf(status)
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = {
dispatcher.send(it).also {
sendAnalyticsRelationValueEvent(analytics)
isParentDismissed.value = true
}
}
)
}
}
fun onAddSelectedValuesToObjectClicked(
ctx: Id,
obj: Id,
relation: Id
) {
val tags = views.value.mapNotNull { view ->
if (view is RelationValueBaseViewModel.RelationValueView.Tag && view.isSelected == true)
view.id
else
null
}
proceedWithAddingTagToObject(
obj = obj,
ctx = ctx,
relation = relation,
tags = tags
)
}
private fun proceedWithAddingTagToObject(
obj: Id,
ctx: Id,
relation: Id,
tags: List<Id>
) {
viewModelScope.launch {
val obj = values.get(target = obj)
val result = mutableListOf<Id>()
val value = obj[relation]
if (value is List<*>) {
result.addAll(value.typeOf())
} else if (value is Id) {
result.add(value)
}
result.addAll(tags)
updateDetail(
UpdateDetail.Params(
ctx = ctx,
key = relation,
value = result
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = {
dispatcher.send(it).also {
sendAnalyticsRelationValueEvent(analytics)
isDismissed.value = true
}
}
)
}
}
fun onCreateObjectRelationOptionClicked(
ctx: Id,
relation: Id,
name: String,
obj: Id
) {
viewModelScope.launch {
addObjectRelationOption(
AddObjectRelationOption.Params(
ctx = ctx,
relation = relation,
name = name,
color = ThemeColor.values().filter { it != ThemeColor.DEFAULT }.random().title
)
).proceed(
success = { (payload, option) ->
dispatcher.send(payload)
if (option != null) {
when (val format = relations.get(relation).format) {
Relation.Format.TAG -> {
proceedWithAddingTagToObject(
ctx = ctx,
relation = relation,
obj = obj,
tags = listOf(option)
)
}
Relation.Format.STATUS -> {
proceedWithAddingStatusToObject(
ctx = ctx,
relation = relation,
status = option
)
}
else -> {
Timber.e("Trying to create an option for relation format: $format")
}
}
}
},
failure = { Timber.e(it, "Error while creating a new option for object") }
)
}
}
class Factory(
private val values: ObjectValueProvider,
private val details: ObjectDetailProvider,
private val relations: ObjectRelationProvider,
private val types: ObjectTypesProvider,
private val addObjectRelationOption: AddObjectRelationOption,
private val updateDetail: UpdateDetail,
private val urlBuilder: UrlBuilder,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return RelationOptionValueAddViewModel(
details = details,
values = values,
relations = relations,
types = types,
urlBuilder = urlBuilder,
addObjectRelationOption = addObjectRelationOption,
updateDetail = updateDetail,
dispatcher = dispatcher,
analytics = analytics
) as T
}
}
}

View file

@ -0,0 +1,206 @@
package com.anytypeio.anytype.presentation.relations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.domain.`object`.ObjectTypesProvider
import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption
import com.anytypeio.anytype.domain.dataview.interactor.AddStatusToDataViewRecord
import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.presentation.editor.editor.ThemeColor
import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider
import com.anytypeio.anytype.presentation.sets.RelationValueBaseViewModel
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.launch
import timber.log.Timber
class RelationOptionValueDVAddViewModel(
details: ObjectDetailProvider,
types: ObjectTypesProvider,
urlBuilder: UrlBuilder,
values: ObjectValueProvider,
relations: ObjectRelationProvider,
private val addDataViewRelationOption: AddDataViewRelationOption,
private val addTagToDataViewRecord: AddTagToDataViewRecord,
private val addStatusToDataViewRecord: AddStatusToDataViewRecord,
private val dispatcher: Dispatcher<Payload>,
) : AddObjectRelationValueViewModel(
details = details,
values = values,
types = types,
urlBuilder = urlBuilder,
relations = relations,
) {
fun onCreateDataViewRelationOptionClicked(
ctx: Id,
dataview: Id,
viewer: Id,
relation: Id,
target: Id,
name: String
) {
viewModelScope.launch {
addDataViewRelationOption(
AddDataViewRelationOption.Params(
ctx = ctx,
relation = relation,
dataview = dataview,
record = target,
name = name,
color = ThemeColor.values().filter { it != ThemeColor.DEFAULT }.random().title
)
).proceed(
success = { (payload, option) ->
dispatcher.send(payload)
if (option != null) {
when (relations.get(relation).format) {
Relation.Format.TAG -> {
proceedWithAddingTagToDataViewRecord(
ctx = ctx,
dataview = dataview,
viewer = viewer,
relation = relation,
target = target,
tags = listOf(option)
)
}
Relation.Format.STATUS -> {
proceedWithAddingStatusToDataViewRecord(
ctx = ctx,
dataview = dataview,
viewer = viewer,
relation = relation,
obj = target,
status = option
)
}
else -> Timber.e("Trying to create option for wrong relation.")
}
}
},
failure = { Timber.e(it, "Error while creating a new option") }
)
}
}
fun onAddObjectSetStatusClicked(
ctx: Id,
dataview: Id,
viewer: Id,
relation: Id,
obj: Id,
status: RelationValueBaseViewModel.RelationValueView.Status
) {
proceedWithAddingStatusToDataViewRecord(ctx, dataview, viewer, relation, obj, status.id)
}
private fun proceedWithAddingStatusToDataViewRecord(
ctx: Id,
dataview: Id,
viewer: Id,
relation: Id,
obj: Id,
status: Id
) {
viewModelScope.launch {
addStatusToDataViewRecord(
AddStatusToDataViewRecord.Params(
ctx = ctx,
dataview = dataview,
viewer = viewer,
relation = relation,
obj = obj,
status = status,
record = values.get(target = obj)
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = { isParentDismissed.value = true }
)
}
}
fun onAddSelectedValuesToDataViewClicked(
ctx: Id,
dataview: Id,
target: Id,
relation: Id,
viewer: Id
) {
val tags = views.value.mapNotNull { view ->
if (view is RelationValueBaseViewModel.RelationValueView.Tag && view.isSelected == true)
view.id
else
null
}
proceedWithAddingTagToDataViewRecord(
target = target,
ctx = ctx,
dataview = dataview,
relation = relation,
viewer = viewer,
tags = tags
)
}
private fun proceedWithAddingTagToDataViewRecord(
target: Id,
ctx: Id,
dataview: Id,
relation: Id,
viewer: Id,
tags: List<Id>
) {
viewModelScope.launch {
val record = values.get(target = target)
addTagToDataViewRecord(
AddTagToDataViewRecord.Params(
ctx = ctx,
tags = tags,
record = record,
dataview = dataview,
relation = relation,
viewer = viewer,
target = target
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = { isDismissed.value = true }
)
}
}
class Factory(
private val values: ObjectValueProvider,
private val details: ObjectDetailProvider,
private val relations: ObjectRelationProvider,
private val types: ObjectTypesProvider,
private val addDataViewRelationOption: AddDataViewRelationOption,
private val addTagToDataViewRecord: AddTagToDataViewRecord,
private val addStatusToDataViewRecord: AddStatusToDataViewRecord,
private val urlBuilder: UrlBuilder,
private val dispatcher: Dispatcher<Payload>,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return RelationOptionValueDVAddViewModel(
details = details,
values = values,
relations = relations,
types = types,
urlBuilder = urlBuilder,
addDataViewRelationOption = addDataViewRelationOption,
addTagToDataViewRecord = addTagToDataViewRecord,
addStatusToDataViewRecord = addStatusToDataViewRecord,
dispatcher = dispatcher,
) as T
}
}
}

View file

@ -0,0 +1,5 @@
import com.anytypeio.anytype.domain.config.Gateway
object FakeGateWay : Gateway {
override fun obtain(): String = "anytype.io"
}

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.presentation.extension
import FakeGateWay
import MockDataFactory
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.domain.config.Gateway
@ -46,14 +47,7 @@ class DashboardViewExtensionKtTest {
)
)
val testGate = object : Gateway {
override fun obtain(): String {
return "anytype.io"
}
}
val builder = UrlBuilder(gateway = testGate)
val builder = UrlBuilder(FakeGateWay)
val result = views.updateDetails(
target = views[1].target,

View file

@ -0,0 +1,152 @@
package com.anytypeio.anytype.presentation.relations
import FakeGateWay
import app.cash.turbine.test
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.core_models.StubRelation
import com.anytypeio.anytype.core_models.StubRelationOption
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.presentation.relations.providers.FakeObjectDetailsProvider
import com.anytypeio.anytype.presentation.relations.providers.FakeObjectRelationProvider
import com.anytypeio.anytype.presentation.relations.providers.FakeObjectTypesProvider
import com.anytypeio.anytype.presentation.relations.providers.FakeObjectValueProvider
import com.anytypeio.anytype.presentation.sets.RelationValueBaseViewModel.RelationValueView
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.test.BeforeTest
import kotlin.test.assertEquals
@ExperimentalCoroutinesApi
class AddObjectRelationValueViewModelTest {
@get:Rule
internal val coroutineTestRule = CoroutinesTestRule()
private val option = StubRelationOption()
private val relation = StubRelation(
format = Relation.Format.TAG,
selections = listOf(option)
)
private val relationsProvider = FakeObjectRelationProvider
@Test
fun `query is empty - there is only tag view`() {
val viewModel = createViewModel()
relationsProvider.relation = relation
runTest {
viewModel.ui.test {
viewModel.onStart("", "")
// first item is emmit in constructor
val actual = listOf(awaitItem(), awaitItem())[1]
assertEquals(
expected = listOf(
RelationValueView.Tag(
id = option.id,
name = option.text,
color = option.color,
isSelected = false
)
),
actual = actual
)
}
}
}
@Test
fun `query doesn't contain tag - there is only create view`() {
val viewModel = createViewModel()
relationsProvider.relation = relation
runTest {
viewModel.ui.test {
viewModel.onStart("", "")
val query = MockDataFactory.randomString() + option.text
viewModel.onFilterInputChanged(query)
// first item is emmit in constructor, second - at onStart
val actual = listOf(awaitItem(), awaitItem(), awaitItem())[2]
assertEquals(
expected = listOf(
RelationValueView.Create(query),
),
actual = actual
)
}
}
}
@Test
fun `query contains tag parts but doesn't equal - there are create and tag views`() {
val viewModel = createViewModel()
relationsProvider.relation = relation
runTest {
viewModel.ui.test {
viewModel.onStart("", "")
val query = option.text.substring(1)
viewModel.onFilterInputChanged(query)
// first item is emmit in constructor, second - at onStart
val actual = listOf(awaitItem(), awaitItem(), awaitItem())[2]
assertEquals(
expected = listOf(
RelationValueView.Create(query),
RelationValueView.Tag(
id = option.id,
name = option.text,
color = option.color,
isSelected = false
)
),
actual = actual
)
}
}
}
@Test
fun `query equals tag - there is only tag view`() {
val viewModel = createViewModel()
relationsProvider.relation = relation
runTest {
viewModel.ui.test {
viewModel.onStart("", "")
// first item is emmit in constructor, second - at onStart
val actual = listOf(awaitItem(), awaitItem())[1]
assertEquals(
expected = listOf(
RelationValueView.Tag(
id = option.id,
name = option.text,
color = option.color,
isSelected = false
)
),
actual = actual
)
val query = option.text
viewModel.onFilterInputChanged(query)
// because `ui` has distinct under the hood
expectNoEvents()
}
}
}
private fun createViewModel(): AddObjectRelationValueViewModel = object :
AddObjectRelationValueViewModel(
FakeObjectValueProvider,
FakeObjectDetailsProvider,
relationsProvider,
FakeObjectTypesProvider,
UrlBuilder(FakeGateWay)
) {}
}

View file

@ -0,0 +1,11 @@
package com.anytypeio.anytype.presentation.relations.providers
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider
internal object FakeObjectDetailsProvider : ObjectDetailProvider {
override fun provide(): Map<Id, Block.Fields> {
return emptyMap()
}
}

View file

@ -0,0 +1,21 @@
package com.anytypeio.anytype.presentation.relations.providers
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.core_models.StubRelation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
internal object FakeObjectRelationProvider : ObjectRelationProvider {
internal var relation: Relation = StubRelation()
override fun get(relation: Id): Relation {
return this.relation
}
override fun subscribe(relationId: Id): Flow<Relation> {
return flow {
emit(relation)
}
}
}

View file

@ -0,0 +1,14 @@
package com.anytypeio.anytype.presentation.relations.providers
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.domain.`object`.ObjectTypesProvider
internal object FakeObjectTypesProvider : ObjectTypesProvider {
internal var list = emptyList<ObjectType>()
override fun get(): List<ObjectType> = list
override fun set(objectTypes: List<ObjectType>) {
this.list = objectTypes
}
}

View file

@ -0,0 +1,13 @@
package com.anytypeio.anytype.presentation.relations.providers
import com.anytypeio.anytype.core_models.Id
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
internal object FakeObjectValueProvider : ObjectValueProvider {
override fun get(target: Id): Map<String, Any?> = emptyMap()
override fun subscribe(target: Id): Flow<Map<String, Any?>> =
flow { emit(emptyMap()) }
}

View file

@ -15,5 +15,8 @@ include ':app',
':analytics',
':protocol',
':core-models',
':test-utils'
include ':ui-settings'
':test:android-utils',
':test:utils',
':test:core-models-stub'
include ':ui-settings'

View file

@ -1 +0,0 @@
/build

View file

@ -1,35 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
def config = rootProject.extensions.getByName("ext")
compileSdkVersion config["compile_sdk"]
defaultConfig {
minSdkVersion config["min_sdk"]
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
}
}
dependencies {
def applicationDependencies = rootProject.ext.mainApplication
def acceptanceTesting = rootProject.ext.acceptanceTesting
implementation applicationDependencies.appcompat
implementation applicationDependencies.kotlin
implementation applicationDependencies.coroutinesAndroid
implementation applicationDependencies.androidxCore
implementation acceptanceTesting.espressoCore
implementation applicationDependencies.design
implementation applicationDependencies.recyclerView
}

View file

@ -0,0 +1,30 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion compile_sdk
defaultConfig {
minSdkVersion min_sdk
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
}
}
dependencies {
implementation mainApplication.appcompat
implementation mainApplication.kotlin
implementation mainApplication.coroutinesAndroid
implementation mainApplication.androidxCore
implementation acceptanceTesting.espressoCore
implementation mainApplication.design
implementation mainApplication.recyclerView
}

View file

@ -75,11 +75,12 @@ fun ViewInteraction.checkHasChildViewCount(count: Int) : ViewInteraction {
return check(matches(WithChildViewCount(count)))
}
fun Int.rVMatcher(): RecyclerViewMatcher = RecyclerViewMatcher(this)
fun Int.rVMatcher(): RecyclerViewMatcher =
RecyclerViewMatcher(this)
fun Int.checkRecyclerItemCount(expected: Int) = matchView().check(RecyclerViewItemCountAssertion(expected))
fun RecyclerViewMatcher.onItemView(pos: Int,target: Int): ViewInteraction {
fun RecyclerViewMatcher.onItemView(pos: Int, target: Int): ViewInteraction {
return onView(atPositionOnView(pos, target))
}

View file

@ -0,0 +1,8 @@
plugins {
id "kotlin"
}
dependencies {
api project(':test:utils')
api project(":core-models")
}

View file

@ -0,0 +1,37 @@
package com.anytypeio.anytype.core_models
import com.anytypeio.anytype.test_utils.MockDataFactory
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 StubRelationOption(
id: String = MockDataFactory.randomUuid(),
text: String = MockDataFactory.randomString(),
color: String = MockDataFactory.randomString(),
scope: Relation.OptionScope = Relation.OptionScope.LOCAL
): Relation.Option =
Relation.Option(
id, text, color, scope
)

7
test/utils/build.gradle Normal file
View file

@ -0,0 +1,7 @@
plugins {
id "kotlin"
}
dependencies {
implementation mainApplication.kotlin
}