mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 13:57:10 +09:00
Refact | Navigation in sets (#1817)
This commit is contained in:
parent
07e83b6ae8
commit
79a5a2f600
9 changed files with 617 additions and 355 deletions
|
@ -292,8 +292,6 @@ open class EditorTestSetup {
|
|||
interceptThreadStatus = interceptThreadStatus,
|
||||
analytics = analytics,
|
||||
dispatcher = Dispatcher.Default(),
|
||||
setDocCoverImage = setDocCoverImage,
|
||||
removeDocCover = removeDocCover,
|
||||
detailModificationManager = InternalDetailModificationManager(stores.details),
|
||||
updateDetail = updateDetail,
|
||||
getCompatibleObjectTypes = getCompatibleObjectTypes,
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.widget.LinearLayout
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_utils.ext.gone
|
||||
|
@ -18,7 +19,7 @@ import timber.log.Timber
|
|||
|
||||
class ViewerGridAdapter(
|
||||
private val onCellClicked: (CellView) -> Unit,
|
||||
private val onObjectHeaderClicked: (String, String?) -> Unit
|
||||
private val onObjectHeaderClicked: (Id) -> Unit
|
||||
) : ListAdapter<Viewer.GridView.Row, ViewerGridAdapter.RecordHolder>(GridDiffUtil) {
|
||||
|
||||
var recordNamePositionX = 0f
|
||||
|
@ -40,7 +41,7 @@ class ViewerGridAdapter(
|
|||
val pos = bindingAdapterPosition
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
val item = getItem(pos)
|
||||
onObjectHeaderClicked(item.id, item.type)
|
||||
onObjectHeaderClicked(item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ ext {
|
|||
junit_version = '4.12'
|
||||
kluent_version = '1.14'
|
||||
timber_junit = '1.0.1'
|
||||
turbine_version = '0.6.1'
|
||||
coroutine_testing_version = '1.4.3'
|
||||
live_data_testing_version = '1.2.0'
|
||||
mockito_kotlin_version = '3.2.0'
|
||||
|
@ -147,7 +148,8 @@ ext {
|
|||
coroutineTesting: "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_testing_version",
|
||||
assertj: "com.squareup.assertj:assertj-android:1.0.0",
|
||||
androidXTestCore: "androidx.test:core:$androidx_test_core_version",
|
||||
timberJUnit: "net.lachlanmckee:timber-junit-rule:$timber_junit"
|
||||
timberJUnit: "net.lachlanmckee:timber-junit-rule:$timber_junit",
|
||||
turbine: "app.cash.turbine:turbine:$turbine_version"
|
||||
]
|
||||
|
||||
acceptanceTesting = [
|
||||
|
|
|
@ -18,5 +18,5 @@ class InterceptThreadStatus(
|
|||
return channel.observe(params.ctx).flowOn(context)
|
||||
}
|
||||
|
||||
class Params(val ctx: Id)
|
||||
data class Params(val ctx: Id)
|
||||
}
|
||||
|
|
|
@ -88,4 +88,5 @@ dependencies {
|
|||
testImplementation unitTestDependencies.androidXTestCore
|
||||
testImplementation unitTestDependencies.robolectricLatest
|
||||
testImplementation unitTestDependencies.timberJUnit
|
||||
testImplementation unitTestDependencies.turbine
|
||||
}
|
||||
|
|
|
@ -30,7 +30,10 @@ import com.anytypeio.anytype.presentation.editor.model.TextUpdate
|
|||
import com.anytypeio.anytype.presentation.mapper.toDomain
|
||||
import com.anytypeio.anytype.presentation.navigation.AppNavigation
|
||||
import com.anytypeio.anytype.presentation.navigation.SupportNavigation
|
||||
import com.anytypeio.anytype.presentation.relations.*
|
||||
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
|
||||
import com.anytypeio.anytype.presentation.relations.render
|
||||
import com.anytypeio.anytype.presentation.relations.tabs
|
||||
import com.anytypeio.anytype.presentation.relations.title
|
||||
import com.anytypeio.anytype.presentation.sets.model.*
|
||||
import com.anytypeio.anytype.presentation.util.Dispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -368,8 +371,8 @@ class ObjectSetViewModel(
|
|||
|
||||
val block = state.dataview
|
||||
val dv = block.content as DV
|
||||
val viewer = dv.viewers.find { it.id == session.currentViewerId }?.id
|
||||
?: dv.viewers.first().id
|
||||
val viewer =
|
||||
dv.viewers.find { it.id == session.currentViewerId }?.id ?: dv.viewers.first().id
|
||||
|
||||
if (dv.isRelationReadOnly(relationKey = cell.key)) {
|
||||
val relation = dv.relations.first { it.key == cell.key }
|
||||
|
@ -377,10 +380,7 @@ class ObjectSetViewModel(
|
|||
// TODO terrible workaround, which must be removed in the future!
|
||||
if (cell is CellView.Object && cell.objects.isNotEmpty()) {
|
||||
val obj = cell.objects.first()
|
||||
onObjectClicked(
|
||||
id = obj.id,
|
||||
types = obj.types
|
||||
)
|
||||
onRelationObjectClicked(target = obj.id)
|
||||
return
|
||||
} else {
|
||||
toast(NOT_ALLOWED_CELL)
|
||||
|
@ -451,58 +451,40 @@ class ObjectSetViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onObjectClicked(id: Id, types: List<String>?) {
|
||||
Timber.d("onObjectClicked, id:[$id], type:[$types]")
|
||||
|
||||
if (types.isNullOrEmpty()) {
|
||||
Timber.e("onObjectClicked, types is null or empty, layout type unknown")
|
||||
toast(OBJECT_TYPE_UNKNOWN)
|
||||
return
|
||||
}
|
||||
|
||||
val targetType = reducer.state.value.objectTypes.getObjectTypeById(types)
|
||||
|
||||
if (targetType != null) {
|
||||
when (targetType.layout) {
|
||||
ObjectType.Layout.BASIC, ObjectType.Layout.PROFILE -> {
|
||||
navigate(EventWrapper(AppNavigation.Command.OpenObject(id)))
|
||||
}
|
||||
ObjectType.Layout.SET -> {
|
||||
viewModelScope.launch {
|
||||
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(id)))
|
||||
}
|
||||
}
|
||||
else -> Timber.d("Unexpected layout: ${targetType.layout}")
|
||||
}
|
||||
} else {
|
||||
Timber.e("onObjectClicked, types is null or empty, layout type unknown")
|
||||
toast(OBJECT_TYPE_UNKNOWN)
|
||||
/**
|
||||
* @param [target] Object is a dependent object, therefore we look for data in details.
|
||||
*/
|
||||
private fun onRelationObjectClicked(target: Id) {
|
||||
Timber.d("onCellObjectClicked, id:[$target]")
|
||||
val set = reducer.state.value
|
||||
if (set.isInitialized) {
|
||||
val obj = ObjectWrapper.Basic(set.details[target]?.map ?: emptyMap())
|
||||
proceedWithNavigation(
|
||||
target = target,
|
||||
layout = obj.layout
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onObjectHeaderClicked(id: Id, type: String?) {
|
||||
Timber.d("onObjectHeaderClicked, id:[$id], type:[$type]")
|
||||
/**
|
||||
* @param [target] object is a record contained in this set.
|
||||
*/
|
||||
fun onObjectHeaderClicked(target: Id) {
|
||||
Timber.d("onObjectHeaderClicked, id:[$target]")
|
||||
val set = reducer.state.value
|
||||
val objectType = set.objectTypes.find { it.url == type }
|
||||
if (objectType == null) {
|
||||
toast("Object type not found: $type")
|
||||
return
|
||||
}
|
||||
when (objectType.layout) {
|
||||
ObjectType.Layout.BASIC,
|
||||
ObjectType.Layout.PROFILE,
|
||||
ObjectType.Layout.TODO,
|
||||
ObjectType.Layout.IMAGE,
|
||||
ObjectType.Layout.FILE -> {
|
||||
navigate(
|
||||
EventWrapper(
|
||||
AppNavigation.Command.OpenObject(
|
||||
id = id
|
||||
)
|
||||
)
|
||||
if (set.isInitialized) {
|
||||
val viewer = session.currentViewerId ?: set.viewers.first().id
|
||||
val records = reducer.state.value.viewerDb[viewer] ?: return
|
||||
val record = records.records.find { rec -> rec[Relations.ID] == target }
|
||||
if (record != null) {
|
||||
val obj = ObjectWrapper.Basic(record)
|
||||
proceedWithNavigation(
|
||||
target = target,
|
||||
layout = obj.layout
|
||||
)
|
||||
} else {
|
||||
toast("Record not found. Please, try again later.")
|
||||
}
|
||||
else -> toast("Routing not implemented for this object type: $objectType")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -909,6 +891,49 @@ class ObjectSetViewModel(
|
|||
|
||||
//endregion
|
||||
|
||||
//region NAVIGATION
|
||||
|
||||
private fun proceedWithNavigation(target: Id, layout: ObjectType.Layout?) {
|
||||
when (layout) {
|
||||
ObjectType.Layout.BASIC,
|
||||
ObjectType.Layout.PROFILE,
|
||||
ObjectType.Layout.TODO,
|
||||
ObjectType.Layout.IMAGE,
|
||||
ObjectType.Layout.FILE -> {
|
||||
viewModelScope.launch {
|
||||
closeBlock(CloseBlock.Params(context)).process(
|
||||
success = {
|
||||
navigate(EventWrapper(AppNavigation.Command.OpenObject(id = target)))
|
||||
},
|
||||
failure = {
|
||||
Timber.e(it, "Error while closing object set: $context")
|
||||
navigate(EventWrapper(AppNavigation.Command.OpenObject(id = target)))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
ObjectType.Layout.SET -> {
|
||||
viewModelScope.launch {
|
||||
closeBlock(CloseBlock.Params(context)).process(
|
||||
success = {
|
||||
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(target)))
|
||||
},
|
||||
failure = {
|
||||
Timber.e(it, "Error while closing object set: $context")
|
||||
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(target)))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
toast("Unexpected layout: $layout")
|
||||
Timber.e("Unexpected layout: $layout")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion NAVIGATION
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
titleUpdateChannel.cancel()
|
||||
|
|
|
@ -6,7 +6,6 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
|||
import com.anytypeio.anytype.core_models.*
|
||||
import com.anytypeio.anytype.core_models.restrictions.DataViewRestriction
|
||||
import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
|
||||
import com.anytypeio.anytype.presentation.navigation.AppNavigation
|
||||
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
|
||||
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
|
||||
import com.anytypeio.anytype.presentation.sets.model.CellView
|
||||
|
@ -44,7 +43,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
|
|||
|
||||
@After
|
||||
fun after() {
|
||||
coroutineTestRule.advanceTime(100)
|
||||
coroutineTestRule.advanceTime(ObjectSetViewModel.TITLE_CHANNEL_DISPATCH_DELAY)
|
||||
}
|
||||
|
||||
val title = Block(
|
||||
|
@ -124,7 +123,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
|
|||
|
||||
val initialRecords = listOf(firstRecord, secondRecord)
|
||||
|
||||
val viewer1 = DVViewer(
|
||||
private val viewer1 = DVViewer(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
filters = emptyList(),
|
||||
sorts = emptyList(),
|
||||
|
@ -133,7 +132,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
|
|||
viewerRelations = vrelations
|
||||
)
|
||||
|
||||
val viewer2 = DVViewer(
|
||||
private val viewer2 = DVViewer(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
filters = emptyList(),
|
||||
sorts = emptyList(),
|
||||
|
@ -161,7 +160,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
|
|||
@Test
|
||||
fun `should show error toast when clicked on read only cell`() = runBlocking {
|
||||
|
||||
val dvRestrictions = listOf<DataViewRestrictions>(
|
||||
val dvRestrictions = listOf(
|
||||
DataViewRestrictions(
|
||||
block = dv.id,
|
||||
restrictions = listOf(DataViewRestriction.VIEWS)
|
||||
|
@ -169,6 +168,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
|
|||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubInterceptThreadStatus()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
|
@ -190,7 +190,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
|
|||
val cell = CellView.Description(
|
||||
id = firstRecordId,
|
||||
key = relations[1].key,
|
||||
text = firstRecord.get(relations[1].key).toString()
|
||||
text = firstRecord[relations[1].key].toString()
|
||||
)
|
||||
|
||||
vm.onGridCellClicked(cell = cell)
|
||||
|
@ -199,294 +199,4 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
|
|||
|
||||
assertEquals(ObjectSetViewModel.NOT_ALLOWED_CELL, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should show error toast on open readonly object as page 1`() = runBlocking {
|
||||
|
||||
val objectTypePage = "_otpage"
|
||||
val objectTypeImage = "_otimage"
|
||||
val objectTypeSet = "_otset"
|
||||
|
||||
val objectTypes = listOf(
|
||||
ObjectType(
|
||||
url = objectTypePage,
|
||||
name = "Page",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.BASIC,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.PAGE),
|
||||
isHidden = false,
|
||||
description = "page",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
),
|
||||
ObjectType(
|
||||
url = objectTypeImage,
|
||||
name = "Image",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.IMAGE,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.FILE),
|
||||
isHidden = false,
|
||||
description = "image",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
),
|
||||
ObjectType(
|
||||
url = objectTypeSet,
|
||||
name = "Set",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.SET,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.SET),
|
||||
isHidden = false,
|
||||
description = "set",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
)
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv
|
||||
),
|
||||
objectTypes = objectTypes
|
||||
)
|
||||
|
||||
stubSetActiveViewer()
|
||||
stubUpdateDataViewViewer()
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.onObjectClicked(id = "dsadas", types = listOf())
|
||||
|
||||
val result = vm.toasts.stream().first()
|
||||
|
||||
assertEquals(ObjectSetViewModel.OBJECT_TYPE_UNKNOWN, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should show error toast on open readonly object as page 2`() = runBlocking {
|
||||
|
||||
val objectTypePage = "_otpage"
|
||||
val objectTypeImage = "_otimage"
|
||||
val objectTypeSet = "_otset"
|
||||
|
||||
val objectTypes = listOf(
|
||||
ObjectType(
|
||||
url = objectTypePage,
|
||||
name = "Page",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.BASIC,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.PAGE),
|
||||
isHidden = false,
|
||||
description = "page",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
),
|
||||
ObjectType(
|
||||
url = objectTypeImage,
|
||||
name = "Image",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.IMAGE,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.FILE),
|
||||
isHidden = false,
|
||||
description = "image",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
),
|
||||
ObjectType(
|
||||
url = objectTypeSet,
|
||||
name = "Set",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.SET,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.SET),
|
||||
isHidden = false,
|
||||
description = "set",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
)
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv
|
||||
),
|
||||
objectTypes = objectTypes
|
||||
)
|
||||
|
||||
stubSetActiveViewer()
|
||||
stubUpdateDataViewViewer()
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.onObjectClicked(id = "dsadas", types = listOf("p6Oa"))
|
||||
|
||||
val result = vm.toasts.stream().first()
|
||||
|
||||
assertEquals(ObjectSetViewModel.OBJECT_TYPE_UNKNOWN, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should proceed with open page when open readonly object as page`() = runBlocking {
|
||||
|
||||
val objectTypePage = "_otpage"
|
||||
val objectTypeImage = "_otimage"
|
||||
val objectTypeSet = "_otset"
|
||||
val objectId = "UnI9A1"
|
||||
|
||||
val objectTypes = listOf(
|
||||
ObjectType(
|
||||
url = objectTypePage,
|
||||
name = "Page",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.BASIC,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.PAGE),
|
||||
isHidden = false,
|
||||
description = "page",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
),
|
||||
ObjectType(
|
||||
url = objectTypeImage,
|
||||
name = "Image",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.IMAGE,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.FILE),
|
||||
isHidden = false,
|
||||
description = "image",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
),
|
||||
ObjectType(
|
||||
url = objectTypeSet,
|
||||
name = "Set",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.SET,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.SET),
|
||||
isHidden = false,
|
||||
description = "set",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
)
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv
|
||||
),
|
||||
objectTypes = objectTypes
|
||||
)
|
||||
|
||||
stubSetActiveViewer()
|
||||
stubUpdateDataViewViewer()
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.onObjectClicked(id = objectId, types = listOf(objectTypePage))
|
||||
|
||||
val result = vm.navigation.value!!.peekContent()
|
||||
|
||||
assertEquals(AppNavigation.Command.OpenObject(id = objectId), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should proceed with open set when open readonly object as set`() = runBlocking {
|
||||
|
||||
val objectTypePage = "_otpage"
|
||||
val objectTypeImage = "_otimage"
|
||||
val objectTypeSet = "_otset"
|
||||
val objectId = "UnI9A1"
|
||||
|
||||
val objectTypes = listOf(
|
||||
ObjectType(
|
||||
url = objectTypePage,
|
||||
name = "Page",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.BASIC,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.PAGE),
|
||||
isHidden = false,
|
||||
description = "page",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
),
|
||||
ObjectType(
|
||||
url = objectTypeImage,
|
||||
name = "Image",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.IMAGE,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.FILE),
|
||||
isHidden = false,
|
||||
description = "image",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
),
|
||||
ObjectType(
|
||||
url = objectTypeSet,
|
||||
name = "Set",
|
||||
relations = listOf(),
|
||||
layout = ObjectType.Layout.SET,
|
||||
emoji = "",
|
||||
smartBlockTypes = listOf(SmartBlockType.SET),
|
||||
isHidden = false,
|
||||
description = "set",
|
||||
isArchived = false,
|
||||
isReadOnly = false
|
||||
)
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv
|
||||
),
|
||||
objectTypes = objectTypes
|
||||
)
|
||||
|
||||
stubSetActiveViewer()
|
||||
stubUpdateDataViewViewer()
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.onObjectClicked(id = objectId, types = listOf(objectTypeSet, objectTypeImage))
|
||||
|
||||
val result = vm.navigation.value!!.peekContent()
|
||||
|
||||
assertEquals(AppNavigation.Command.OpenObjectSet(target = objectId), result)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,510 @@
|
|||
package com.anytypeio.anytype.presentation.sets.main
|
||||
|
||||
import MockDataFactory
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import app.cash.turbine.test
|
||||
import com.anytypeio.anytype.core_models.*
|
||||
import com.anytypeio.anytype.core_models.ext.content
|
||||
import com.anytypeio.anytype.domain.page.CloseBlock
|
||||
import com.anytypeio.anytype.presentation.navigation.AppNavigation
|
||||
import com.anytypeio.anytype.presentation.objects.SupportedLayouts
|
||||
import com.anytypeio.anytype.presentation.sets.ObjectSetCommand
|
||||
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
|
||||
import com.anytypeio.anytype.presentation.sets.model.Viewer
|
||||
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
|
||||
import com.jraska.livedata.test
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verifyBlocking
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
class ObjectSetNavigationTest : ObjectSetViewModelTestSetup() {
|
||||
|
||||
@get:Rule
|
||||
val rule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val coroutineTestRule = CoroutinesTestRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.openMocks(this)
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
coroutineTestRule.advanceTime(ObjectSetViewModel.TITLE_CHANNEL_DISPATCH_DELAY)
|
||||
}
|
||||
|
||||
private val title = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
content = Block.Content.Text(
|
||||
style = Block.Content.Text.Style.TITLE,
|
||||
text = MockDataFactory.randomString(),
|
||||
marks = emptyList()
|
||||
),
|
||||
children = emptyList(),
|
||||
fields = Block.Fields.empty()
|
||||
)
|
||||
|
||||
private val header = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
content = Block.Content.Layout(
|
||||
type = Block.Content.Layout.Type.HEADER
|
||||
),
|
||||
fields = Block.Fields.empty(),
|
||||
children = listOf(title.id)
|
||||
)
|
||||
|
||||
private val linkedProjectRelation = Relation(
|
||||
key = MockDataFactory.randomString(),
|
||||
name = "Linked objects",
|
||||
source = Relation.Source.values().random(),
|
||||
defaultValue = null,
|
||||
format = Relation.Format.OBJECT,
|
||||
isHidden = false,
|
||||
isMulti = true,
|
||||
isReadOnly = false,
|
||||
selections = emptyList()
|
||||
)
|
||||
|
||||
private val objectRelations = listOf(linkedProjectRelation)
|
||||
|
||||
val viewerRelations = objectRelations.map { relation ->
|
||||
DVViewerRelation(
|
||||
key = relation.key,
|
||||
isVisible = true
|
||||
)
|
||||
}
|
||||
|
||||
private val viewer = DVViewer(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
filters = emptyList(),
|
||||
sorts = emptyList(),
|
||||
type = Block.Content.DataView.Viewer.Type.GRID,
|
||||
name = MockDataFactory.randomString(),
|
||||
viewerRelations = viewerRelations
|
||||
)
|
||||
|
||||
private val dv = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
content = DV(
|
||||
source = MockDataFactory.randomString(),
|
||||
relations = objectRelations,
|
||||
viewers = listOf(viewer)
|
||||
),
|
||||
children = emptyList(),
|
||||
fields = Block.Fields.empty()
|
||||
)
|
||||
|
||||
@ExperimentalTime
|
||||
@Test
|
||||
fun `should emit navigation command for editing relation-object cell`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val linkedProjectTargetId = "Linked project ID"
|
||||
val firstRecordId = "First record ID"
|
||||
|
||||
val record = mapOf(
|
||||
Relations.ID to firstRecordId,
|
||||
linkedProjectRelation.key to linkedProjectTargetId
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubInterceptThreadStatus()
|
||||
stubSetActiveViewer()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv
|
||||
),
|
||||
dataViewRestrictions = emptyList(),
|
||||
additionalEvents = listOf(
|
||||
Event.Command.DataView.SetRecords(
|
||||
records = listOf(record),
|
||||
view = viewer.id,
|
||||
id = dv.id,
|
||||
total = 1,
|
||||
context = root
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
// TESTING
|
||||
|
||||
val state = vm.viewerGrid.value
|
||||
|
||||
assertIs<Viewer.GridView>(state)
|
||||
|
||||
assertEquals(
|
||||
expected = 1,
|
||||
actual = state.rows.size
|
||||
)
|
||||
|
||||
// Clicking on cell with linked projects.
|
||||
|
||||
runBlocking {
|
||||
vm.commands.test {
|
||||
vm.onGridCellClicked(state.rows.first().cells.last())
|
||||
assertEquals(
|
||||
awaitItem(),
|
||||
ObjectSetCommand.Modal.EditRelationCell(
|
||||
ctx = root,
|
||||
dataview = dv.id,
|
||||
target = firstRecordId,
|
||||
viewer = viewer.id,
|
||||
relation = linkedProjectRelation.key
|
||||
)
|
||||
)
|
||||
cancelAndConsumeRemainingEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit navigation command for opening an object contained in given relation if this relation is read-only and object's layout is supported`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val supportedObjectLayouts = listOf(
|
||||
ObjectType.Layout.BASIC,
|
||||
ObjectType.Layout.PROFILE,
|
||||
ObjectType.Layout.TODO,
|
||||
ObjectType.Layout.IMAGE,
|
||||
ObjectType.Layout.FILE
|
||||
)
|
||||
|
||||
val linkedProjectTargetId = "Linked project ID"
|
||||
val firstRecordId = "First record ID"
|
||||
|
||||
val record = mapOf(
|
||||
Relations.ID to firstRecordId,
|
||||
linkedProjectRelation.key to linkedProjectTargetId
|
||||
)
|
||||
|
||||
val details = Block.Details(
|
||||
details = mapOf(
|
||||
linkedProjectTargetId to Block.Fields(
|
||||
mapOf(
|
||||
Relations.ID to linkedProjectTargetId,
|
||||
Relations.LAYOUT to supportedObjectLayouts.random().code.toDouble()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubInterceptThreadStatus()
|
||||
stubSetActiveViewer()
|
||||
stubCloseBlock()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv.copy(
|
||||
content = dv.content<DV>().copy(
|
||||
relations = listOf(
|
||||
linkedProjectRelation.copy(
|
||||
isReadOnly = true
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
dataViewRestrictions = emptyList(),
|
||||
additionalEvents = listOf(
|
||||
Event.Command.DataView.SetRecords(
|
||||
records = listOf(record),
|
||||
view = viewer.id,
|
||||
id = dv.id,
|
||||
total = 1,
|
||||
context = root
|
||||
)
|
||||
),
|
||||
details = details
|
||||
)
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
// TESTING
|
||||
|
||||
val state = vm.viewerGrid.value
|
||||
|
||||
assertIs<Viewer.GridView>(state)
|
||||
|
||||
assertEquals(
|
||||
expected = 1,
|
||||
actual = state.rows.size
|
||||
)
|
||||
|
||||
// Clicking on cell with linked projects.
|
||||
|
||||
val testObserver = vm.navigation.test()
|
||||
|
||||
vm.onGridCellClicked(state.rows.first().cells.last())
|
||||
|
||||
testObserver.assertValue { value ->
|
||||
val content = value.peekContent()
|
||||
content == AppNavigation.Command.OpenObject(linkedProjectTargetId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should close current object before navitating to some other object`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val linkedProjectTargetId = "Linked project ID"
|
||||
val firstRecordId = "First record ID"
|
||||
|
||||
val record = mapOf(
|
||||
Relations.ID to firstRecordId,
|
||||
linkedProjectRelation.key to linkedProjectTargetId
|
||||
)
|
||||
|
||||
val details = Block.Details(
|
||||
details = mapOf(
|
||||
linkedProjectTargetId to Block.Fields(
|
||||
mapOf(
|
||||
Relations.ID to linkedProjectTargetId,
|
||||
Relations.LAYOUT to SupportedLayouts.layouts.random().code.toDouble()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubInterceptThreadStatus()
|
||||
stubSetActiveViewer()
|
||||
stubCloseBlock()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv.copy(
|
||||
content = dv.content<DV>().copy(
|
||||
relations = listOf(
|
||||
linkedProjectRelation.copy(
|
||||
isReadOnly = true
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
dataViewRestrictions = emptyList(),
|
||||
additionalEvents = listOf(
|
||||
Event.Command.DataView.SetRecords(
|
||||
records = listOf(record),
|
||||
view = viewer.id,
|
||||
id = dv.id,
|
||||
total = 1,
|
||||
context = root
|
||||
)
|
||||
),
|
||||
details = details
|
||||
)
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
// TESTING
|
||||
|
||||
val state = vm.viewerGrid.value
|
||||
|
||||
assertIs<Viewer.GridView>(state)
|
||||
|
||||
assertEquals(
|
||||
expected = 1,
|
||||
actual = state.rows.size
|
||||
)
|
||||
|
||||
// Clicking on cell with linked projects.
|
||||
|
||||
vm.onGridCellClicked(state.rows.first().cells.last())
|
||||
|
||||
verifyBlocking(closeBlock, times(1)) {
|
||||
invoke(
|
||||
CloseBlock.Params(root)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not emit any navigation command for opening an object contained in given relation if object's layout is not supported`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val unsupportedLayouis = ObjectType.Layout.values().toList() - SupportedLayouts.layouts
|
||||
|
||||
val linkedProjectTargetId = "Linked project ID"
|
||||
val firstRecordId = "First record ID"
|
||||
|
||||
val record = mapOf(
|
||||
Relations.ID to firstRecordId,
|
||||
linkedProjectRelation.key to linkedProjectTargetId
|
||||
)
|
||||
|
||||
val details = Block.Details(
|
||||
details = mapOf(
|
||||
linkedProjectTargetId to Block.Fields(
|
||||
mapOf(
|
||||
Relations.ID to linkedProjectTargetId,
|
||||
Relations.LAYOUT to unsupportedLayouis.random().code.toDouble()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubInterceptThreadStatus()
|
||||
stubSetActiveViewer()
|
||||
stubCloseBlock()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv.copy(
|
||||
content = dv.content<DV>().copy(
|
||||
relations = listOf(
|
||||
linkedProjectRelation.copy(
|
||||
isReadOnly = true
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
dataViewRestrictions = emptyList(),
|
||||
additionalEvents = listOf(
|
||||
Event.Command.DataView.SetRecords(
|
||||
records = listOf(record),
|
||||
view = viewer.id,
|
||||
id = dv.id,
|
||||
total = 1,
|
||||
context = root
|
||||
)
|
||||
),
|
||||
details = details
|
||||
)
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
// TESTING
|
||||
|
||||
val state = vm.viewerGrid.value
|
||||
|
||||
assertIs<Viewer.GridView>(state)
|
||||
|
||||
assertEquals(
|
||||
expected = 1,
|
||||
actual = state.rows.size
|
||||
)
|
||||
|
||||
// Clicking on cell with linked projects.
|
||||
|
||||
val testObserver = vm.navigation.test()
|
||||
|
||||
vm.onGridCellClicked(state.rows.first().cells.last())
|
||||
|
||||
testObserver.assertNoValue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit navigation command opening an object set contained in given relation if this relation is read-only`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val linkedProjectTargetId = "Linked project ID"
|
||||
val firstRecordId = "First record ID"
|
||||
|
||||
val record = mapOf(
|
||||
Relations.ID to firstRecordId,
|
||||
linkedProjectRelation.key to linkedProjectTargetId
|
||||
)
|
||||
|
||||
val details = Block.Details(
|
||||
details = mapOf(
|
||||
linkedProjectTargetId to Block.Fields(
|
||||
mapOf(
|
||||
Relations.ID to linkedProjectTargetId,
|
||||
Relations.LAYOUT to ObjectType.Layout.SET.code.toDouble()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubInterceptThreadStatus()
|
||||
stubSetActiveViewer()
|
||||
stubCloseBlock()
|
||||
stubOpenObjectSet(
|
||||
doc = listOf(
|
||||
header,
|
||||
title,
|
||||
dv.copy(
|
||||
content = dv.content<DV>().copy(
|
||||
relations = listOf(
|
||||
linkedProjectRelation.copy(
|
||||
isReadOnly = true
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
dataViewRestrictions = emptyList(),
|
||||
additionalEvents = listOf(
|
||||
Event.Command.DataView.SetRecords(
|
||||
records = listOf(record),
|
||||
view = viewer.id,
|
||||
id = dv.id,
|
||||
total = 1,
|
||||
context = root
|
||||
)
|
||||
),
|
||||
details = details
|
||||
)
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
// TESTING
|
||||
|
||||
val state = vm.viewerGrid.value
|
||||
|
||||
assertIs<Viewer.GridView>(state)
|
||||
|
||||
assertEquals(
|
||||
expected = 1,
|
||||
actual = state.rows.size
|
||||
)
|
||||
|
||||
// Clicking on cell with linked projects.
|
||||
|
||||
val testObserver = vm.navigation.test()
|
||||
|
||||
vm.onGridCellClicked(state.rows.first().cells.last())
|
||||
|
||||
testObserver.assertValue { value ->
|
||||
val content = value.peekContent()
|
||||
content == AppNavigation.Command.OpenObjectSet(linkedProjectTargetId)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import com.anytypeio.anytype.presentation.sets.ObjectSetSession
|
|||
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
|
||||
import com.anytypeio.anytype.presentation.util.Dispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.mockito.Mock
|
||||
import org.mockito.kotlin.any
|
||||
|
@ -101,6 +102,14 @@ open class ObjectSetViewModelTestSetup {
|
|||
}
|
||||
}
|
||||
|
||||
fun stubInterceptThreadStatus(
|
||||
params: InterceptThreadStatus.Params = InterceptThreadStatus.Params(ctx = root)
|
||||
) {
|
||||
interceptThreadStatus.stub {
|
||||
onBlocking { build(params) } doReturn emptyFlow()
|
||||
}
|
||||
}
|
||||
|
||||
fun stubOpenObjectSet(
|
||||
doc: List<Block> = emptyList(),
|
||||
details: Block.Details = Block.Details(),
|
||||
|
@ -156,4 +165,10 @@ open class ObjectSetViewModelTestSetup {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun stubCloseBlock() {
|
||||
closeBlock.stub {
|
||||
onBlocking { invoke(any()) } doReturn Either.Right(Unit)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue