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

DROID-497 Editor | Feature | Add link to self from object or set to another object (#2668)

This commit is contained in:
Mikhail 2022-10-24 22:14:19 +03:00 committed by GitHub
parent 68764be5e9
commit 8cf9117cb6
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 546 additions and 269 deletions

View file

@ -59,6 +59,7 @@ object EventsDictionary {
const val objectRemoveCover = "RemoveCover"
const val objectAddToFavorites = "AddToFavorites"
const val objectRemoveFromFavorites = "RemoveFromFavorites"
const val objectLinkTo = "LinkObjectTo"
const val objectMoveToBin = "MoveToBin"
const val objectRelationFeature = "FeatureRelation"
const val objectRelationUnfeature = "UnfeatureRelation"

View file

@ -24,6 +24,7 @@ import com.anytypeio.anytype.features.editor.base.TestEditorFragment
import com.anytypeio.anytype.presentation.MockBlockContentFactory.StubTextContent
import com.anytypeio.anytype.presentation.MockBlockFactory
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import com.anytypeio.anytype.test_utils.utils.TestUtils
import com.anytypeio.anytype.ui.editor.EditorFragment
import com.anytypeio.anytype.utils.CoroutinesTestRule
@ -32,6 +33,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
@ -323,8 +325,8 @@ class CreateBlockTesting : EditorTestSetup() {
) {
createBlock.stub {
onBlocking {
invoke(params)
} doReturn Either.Right(
execute(params)
} doAnswer ValueClassAnswer(
Pair(new.id, Payload(context = root, events = events))
)
}

View file

@ -25,12 +25,14 @@ import com.anytypeio.anytype.features.editor.base.TestEditorFragment
import com.anytypeio.anytype.presentation.MockBlockContentFactory.StubTextContent
import com.anytypeio.anytype.presentation.MockBlockFactory
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import com.anytypeio.anytype.test_utils.utils.TestUtils
import com.anytypeio.anytype.ui.editor.EditorFragment
import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
@ -364,8 +366,8 @@ class ListBlockTesting : EditorTestSetup() {
) {
createBlock.stub {
onBlocking {
invoke(params)
} doReturn Either.Right(
execute(params)
} doAnswer ValueClassAnswer(
Pair(new.id, Payload(context = root, events = events))
)
}

View file

@ -17,6 +17,7 @@ import com.anytypeio.anytype.features.editor.base.TestEditorFragment
import com.anytypeio.anytype.presentation.MockBlockContentFactory.StubTextContent
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import com.anytypeio.anytype.test_utils.utils.checkHasText
import com.anytypeio.anytype.test_utils.utils.onItemView
import com.anytypeio.anytype.test_utils.utils.rVMatcher
@ -28,6 +29,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
@ -112,7 +114,7 @@ class MentionUpdateTesting : EditorTestSetup() {
stubInterceptThreadStatus()
stubUpdateText()
openPage.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
onBlocking { execute(any()) } doAnswer ValueClassAnswer(
Result.Success(
Payload(
context = root,

View file

@ -98,12 +98,14 @@ import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
@ -436,7 +438,7 @@ open class EditorTestSetup {
relations: List<Relation> = emptyList()
) {
openPage.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
onBlocking { execute(any()) } doAnswer ValueClassAnswer(
Result.Success(
Payload(
context = root,
@ -460,7 +462,7 @@ open class EditorTestSetup {
events: List<Event.Command>
) {
createBlock.stub {
onBlocking { invoke(params) } doReturn Either.Right(
onBlocking { execute(params) } doAnswer ValueClassAnswer(
Pair(
MockDataFactory.randomUuid(),
Payload(context = root, events = events)

View file

@ -11,9 +11,15 @@ import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.google.android.material.snackbar.Snackbar
fun View.showActionableSnackBar(from: String?, to: String?, icon: ObjectIcon, click: () -> Unit) {
fun View.showActionableSnackBar(
from: String?,
to: String?,
icon: ObjectIcon,
anchor: View? = null,
click: () -> Unit
) {
val snackbar: Snackbar = Snackbar.make(this, "", Snackbar.LENGTH_SHORT)
val snackbar: Snackbar = Snackbar.make(this, "", Snackbar.LENGTH_LONG)
snackbar.view.setBackgroundColor(Color.TRANSPARENT)
val snackbarLayout: Snackbar.SnackbarLayout = snackbar.view as Snackbar.SnackbarLayout
@ -48,13 +54,14 @@ fun View.showActionableSnackBar(from: String?, to: String?, icon: ObjectIcon, cl
}
snackbarLayout.addView(newView, 0)
snackbar.anchorView = anchor
snackbar.show()
}
fun View.showMessageSnackBar(text: String, anchor: View? = null) {
val snackbar: Snackbar = Snackbar.make(this, "", Snackbar.LENGTH_SHORT)
val snackbar: Snackbar = Snackbar.make(this, "", Snackbar.LENGTH_LONG)
snackbar.view.setBackgroundColor(Color.TRANSPARENT)
val snackbarLayout: Snackbar.SnackbarLayout = snackbar.view as Snackbar.SnackbarLayout

View file

@ -4,11 +4,17 @@ 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.`object`.DuplicateObject
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.block.interactor.CreateBlock
import com.anytypeio.anytype.domain.block.interactor.UpdateFields
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.domain.page.AddBackLinkToObject
import com.anytypeio.anytype.domain.page.CloseBlock
import com.anytypeio.anytype.domain.page.OpenPage
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.editor.Editor
@ -67,6 +73,15 @@ object ObjectMenuModuleBase {
fun provideRemoveFromFavoriteUseCase(
repo: BlockRepository
): RemoveFromFavorite = RemoveFromFavorite(repo = repo)
@JvmStatic
@Provides
@PerDialog
fun provideAddBackLinkToObject(
openPage: OpenPage,
createBlock: CreateBlock,
closeBlock: CloseBlock,
): AddBackLinkToObject = AddBackLinkToObject(openPage, createBlock, closeBlock)
}
@Module
@ -79,6 +94,8 @@ object ObjectMenuModule {
duplicateObject: DuplicateObject,
addToFavorite: AddToFavorite,
removeFromFavorite: RemoveFromFavorite,
addBackLinkToObject: AddBackLinkToObject,
urlBuilder: UrlBuilder,
storage: Editor.Storage,
analytics: Analytics,
dispatcher: Dispatcher<Payload>,
@ -89,6 +106,8 @@ object ObjectMenuModule {
duplicateObject = duplicateObject,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
addBackLinkToObject = addBackLinkToObject,
urlBuilder = urlBuilder,
storage = storage,
analytics = analytics,
dispatcher = dispatcher,
@ -115,6 +134,9 @@ object ObjectSetMenuModule {
setObjectIsArchived: SetObjectIsArchived,
addToFavorite: AddToFavorite,
removeFromFavorite: RemoveFromFavorite,
addBackLinkToObject: AddBackLinkToObject,
delegator: Delegator<Action>,
urlBuilder: UrlBuilder,
analytics: Analytics,
state: StateFlow<ObjectSet>,
dispatcher: Dispatcher<Payload>
@ -122,12 +144,32 @@ object ObjectSetMenuModule {
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
addBackLinkToObject = addBackLinkToObject,
urlBuilder = urlBuilder,
delegator = delegator,
analytics = analytics,
state = state,
dispatcher = dispatcher,
menuOptionsProvider = createMenuOptionsProvider(state)
)
@JvmStatic
@Provides
@PerDialog
fun provideOpenPage(
repo: BlockRepository,
auth: AuthRepository
): OpenPage = OpenPage(repo, auth)
@JvmStatic
@Provides
@PerDialog
fun provideCreateBlock(
repo: BlockRepository
): CreateBlock = CreateBlock(
repo = repo
)
@JvmStatic
private fun createMenuOptionsProvider(state: StateFlow<ObjectSet>) =
ObjectMenuOptionsProviderImpl(

View file

@ -955,7 +955,8 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
isArchived = command.isArchived,
isFavorite = command.isFavorite,
isLocked = command.isLocked,
isProfile = false
isProfile = false,
fromName = getFrom()
)
fr.show(childFragmentManager, null)
}
@ -1094,14 +1095,11 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
}
}
is Command.OpenObjectSnackbar -> {
val from = (blockAdapter.views
.firstOrNull { it is BlockView.TextSupport } as? BlockView.TextSupport)
?.text
binding.root.showActionableSnackBar(from, command.text, command.icon) {
binding.root.showActionableSnackBar(getFrom(), command.text, command.icon) {
if (command.isSet) {
vm.proceedWithOpeningSet(command.id)
} else {
vm.proceedWithOpeningPage(command.id)
vm.proceedWithOpeningObject(command.id)
}
}
}
@ -1183,6 +1181,10 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
}
}
private fun getFrom() = (blockAdapter.views
.firstOrNull { it is BlockView.TextSupport } as? BlockView.TextSupport)
?.text
private fun openFileByDefaultApp(uri: Uri) {
try {
val intent = Intent(Intent.ACTION_VIEW, uri)

View file

@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.features.objects.ObjectActionAdapter
import com.anytypeio.anytype.core_ui.layout.SpacingItemDecoration
import com.anytypeio.anytype.core_ui.reactive.clicks
@ -16,24 +17,30 @@ import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment
import com.anytypeio.anytype.core_utils.ui.showActionableSnackBar
import com.anytypeio.anytype.databinding.FragmentObjectMenuBinding
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuOptionsProvider
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuViewModelBase
import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectFragment
import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectSetFragment
import com.anytypeio.anytype.ui.editor.layout.ObjectLayoutFragment
import com.anytypeio.anytype.ui.editor.modals.IconPickerFragmentBase
import com.anytypeio.anytype.ui.moving.MoveToFragment
import com.anytypeio.anytype.ui.moving.OnMoveToAction
import com.anytypeio.anytype.ui.relations.RelationListFragment
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
abstract class ObjectMenuBaseFragment : BaseBottomSheetFragment<FragmentObjectMenuBinding>() {
abstract class ObjectMenuBaseFragment : BaseBottomSheetFragment<FragmentObjectMenuBinding>(),
OnMoveToAction {
protected val ctx get() = arg<String>(CTX_KEY)
protected val ctx get() = arg<Id>(CTX_KEY)
private val isProfile get() = arg<Boolean>(IS_PROFILE_KEY)
private val isArchived get() = arg<Boolean>(IS_ARCHIVED_KEY)
private val isFavorite get() = arg<Boolean>(IS_FAVORITE_KEY)
private val isLocked get() = arg<Boolean>(IS_LOCKED_KEY)
private val fromName get() = arg<String?>(FROM_NAME)
abstract val vm: ObjectMenuViewModelBase
@ -171,9 +178,45 @@ abstract class ObjectMenuBaseFragment : BaseBottomSheetFragment<FragmentObjectMe
ObjectMenuViewModelBase.Command.OpenSetRelations -> {
toast(COMING_SOON_MSG)
}
ObjectMenuViewModelBase.Command.OpenLinkToChooser -> {
val fr = MoveToFragment.new(
ctx = ctx,
blocks = emptyList(),
restorePosition = null,
restoreBlock = null,
title = getString(R.string.link_to)
)
fr.show(childFragmentManager, null)
}
is ObjectMenuViewModelBase.Command.OpenSnackbar -> {
binding.root.postDelayed({
dialog?.window
?.decorView
?.showActionableSnackBar(
command.currentObjectName,
command.targetObjectName,
command.icon,
binding.anchor
) {
vm.proceedWithOpeningPage(command.id)
}
}, 300L)
}
}
}
override fun onMoveTo(
target: Id,
blocks: List<Id>,
text: String,
icon: ObjectIcon,
isSet: Boolean
) {
vm.onLinkedMyselfTo(myself = ctx, addTo = target, fromName)
}
override fun onMoveToClose(blocks: List<Id>, restorePosition: Int?, restoreBlock: Id?) {}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
@ -187,6 +230,7 @@ abstract class ObjectMenuBaseFragment : BaseBottomSheetFragment<FragmentObjectMe
const val IS_PROFILE_KEY = "arg.doc-menu-bottom-sheet.is-profile"
const val IS_FAVORITE_KEY = "arg.doc-menu-bottom-sheet.is-favorite"
const val IS_LOCKED_KEY = "arg.doc-menu-bottom-sheet.is-locked"
const val FROM_NAME = "arg.doc-menu-bottom-sheet.from-name"
const val COMING_SOON_MSG = "Coming soon..."
}

View file

@ -42,14 +42,16 @@ class ObjectMenuFragment : ObjectMenuBaseFragment() {
isArchived: Boolean,
isProfile: Boolean,
isFavorite: Boolean,
isLocked: Boolean
isLocked: Boolean,
fromName: String?
) = ObjectMenuFragment().apply {
arguments = bundleOf(
CTX_KEY to ctx,
IS_ARCHIVED_KEY to isArchived,
IS_PROFILE_KEY to isProfile,
IS_FAVORITE_KEY to isFavorite,
IS_LOCKED_KEY to isLocked
IS_LOCKED_KEY to isLocked,
FROM_NAME to fromName
)
}
}

View file

@ -49,6 +49,7 @@ class MoveToFragment : BaseBottomSheetTextInputFragment<FragmentObjectSearchBind
private val ctx get() = arg<Id>(ARG_CTX)
private val restorePosition get() = argOrNull<Int>(ARG_RESTORE_POSITION)
private val restoreBlock get() = argOrNull<Id>(ARG_RESTORE_BLOCK)
private val title get() = argOrNull<String>(ARG_TITLE)
private val moveToAdapter by lazy {
DefaultObjectViewAdapter(
@ -196,7 +197,11 @@ class MoveToFragment : BaseBottomSheetTextInputFragment<FragmentObjectSearchBind
private fun initialize() {
with(binding.tvScreenTitle) {
text = getString(R.string.move_to)
if (title != null) {
text = title
} else {
text = getString(R.string.move_to)
}
visible()
}
binding.recyclerView.invisible()
@ -261,19 +266,22 @@ class MoveToFragment : BaseBottomSheetTextInputFragment<FragmentObjectSearchBind
const val ARG_CTX = "arg.move_to.ctx"
const val ARG_RESTORE_POSITION = "arg.move_to.position"
const val ARG_RESTORE_BLOCK = "arg.move_to.restore_block"
const val ARG_TITLE = "arg.move_to.title"
const val EMPTY_FILTER_TEXT = ""
fun new(
ctx: Id,
blocks: List<Id>,
restorePosition: Int?,
restoreBlock: Id?
restoreBlock: Id?,
title: String? = null
) = MoveToFragment().apply {
arguments = bundleOf(
ARG_CTX to ctx,
ARG_BLOCKS to blocks,
ARG_RESTORE_POSITION to restorePosition,
ARG_RESTORE_BLOCK to restoreBlock
ARG_RESTORE_BLOCK to restoreBlock,
ARG_TITLE to title
)
}
}

View file

@ -570,7 +570,8 @@ open class ObjectSetFragment :
ObjectMenuBaseFragment.IS_ARCHIVED_KEY to command.isArchived,
ObjectMenuBaseFragment.IS_FAVORITE_KEY to command.isFavorite,
ObjectMenuBaseFragment.IS_PROFILE_KEY to false,
ObjectMenuBaseFragment.IS_LOCKED_KEY to false
ObjectMenuBaseFragment.IS_LOCKED_KEY to false,
ObjectMenuBaseFragment.FROM_NAME to title.text.toString()
)
)
}

View file

@ -154,4 +154,11 @@
</FrameLayout>
<View
android:id="@+id/anchor"
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -68,6 +68,10 @@ class ObjectActionAdapter(
ivActionIcon.setImageResource(R.drawable.ic_object_action_unlock)
tvActionTitle.setText(R.string.unlock)
}
ObjectAction.LINK_TO -> {
ivActionIcon.setImageResource(R.drawable.ic_object_action_link_to)
tvActionTitle.setText(R.string.link_to)
}
ObjectAction.MOVE_TO -> {
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"
android:pathData="M19.536,7.117C20.821,5.832 20.821,3.749 19.536,2.464C18.251,1.179 16.168,1.179 14.883,2.464C13.599,3.749 13.599,5.832 14.883,7.117C16.168,8.402 18.251,8.402 19.536,7.117ZM20.597,8.177C22.468,6.307 22.468,3.274 20.597,1.403C18.726,-0.468 15.693,-0.468 13.823,1.403C12.302,2.924 12.017,5.214 12.97,7.023C13.207,7.473 13.186,8.041 12.827,8.4L8.4,12.827C8.041,13.186 7.473,13.207 7.023,12.97C5.214,12.017 2.924,12.302 1.403,13.823C-0.468,15.693 -0.468,18.726 1.403,20.597C3.274,22.468 6.307,22.468 8.177,20.597C9.641,19.134 9.959,16.959 9.133,15.186C8.928,14.745 8.963,14.208 9.307,13.865L13.865,9.307C14.208,8.963 14.745,8.928 15.186,9.133C16.959,9.959 19.134,9.641 20.597,8.177ZM7.117,19.536C8.402,18.251 8.402,16.168 7.117,14.883C5.832,13.599 3.749,13.599 2.464,14.883C1.179,16.168 1.179,18.251 2.464,19.536C3.749,20.821 5.832,20.821 7.117,19.536Z" />
</vector>

View file

@ -58,3 +58,13 @@ abstract class ResultInteractor<in P, R> {
protected abstract suspend fun doWork(params: P): R
}
suspend fun <R, T> Result<T>.suspendFold(
onSuccess: suspend (value: T) -> R,
onFailure: suspend (Throwable) -> R
): R {
return when (val exception = exceptionOrNull()) {
null -> onSuccess(getOrNull() as T)
else -> onFailure(exception)
}
}

View file

@ -1,14 +1,14 @@
package com.anytypeio.anytype.domain.block.interactor
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.block.interactor.CreateBlock.Params
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Position
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Position
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.interactor.CreateBlock.Params
import com.anytypeio.anytype.domain.block.repo.BlockRepository
/**
* Use-case for creating a block.
@ -16,9 +16,9 @@ import com.anytypeio.anytype.core_models.Payload
*/
open class CreateBlock(
private val repo: BlockRepository
) : BaseUseCase<Pair<Id, Payload>, Params>() {
) : ResultInteractor<Params, Pair<Id, Payload>>() {
override suspend fun run(params: Params) = try {
override suspend fun doWork(params: Params): Pair<Id, Payload> =
repo.create(
command = Command.Create(
context = params.context,
@ -26,12 +26,7 @@ open class CreateBlock(
prototype = params.prototype,
position = params.position
)
).let {
Either.Right(it)
}
} catch (t: Throwable) {
Either.Left(t)
}
)
/**
* Params for creating a block

View file

@ -0,0 +1,62 @@
package com.anytypeio.anytype.domain.page
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Position
import com.anytypeio.anytype.domain.base.Result
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.interactor.CreateBlock
/**
* Add backlink from set or object itself to another object as a last block
*/
class AddBackLinkToObject(
private val openPage: OpenPage,
private val createBlock: CreateBlock,
private val closeBlock: CloseBlock,
) : ResultInteractor<AddBackLinkToObject.Params, ObjectWrapper.Basic>() {
override suspend fun doWork(params: Params): ObjectWrapper.Basic {
when (val result = openPage.run(params.objectToPlaceLink)) {
is Result.Success -> {
val event = result.data
.events
.firstOrNull { event -> event is Event.Command.ShowObject }
check(event is Event.Command.ShowObject) { "Event ShowObject is missing" }
val targetBlock = event.blocks
.firstOrNull { it.id == params.objectToPlaceLink }
?.children
?.last()
val objectDetails = event.details.details[params.objectToPlaceLink]?.map
require(targetBlock != null) { "Target block is missing" }
require(objectDetails != null) { "Object details is missing" }
createBlock.run(
CreateBlock.Params(
context = params.objectToPlaceLink,
target = targetBlock,
position = Position.BOTTOM,
prototype = Block.Prototype.Link(params.objectToLink)
)
)
closeBlock.run(params.objectToPlaceLink)
return ObjectWrapper.Basic(objectDetails)
}
is Result.Failure -> throw IllegalStateException("object open error ${result.error}")
}
}
data class Params(
val objectToLink: Id,
val objectToPlaceLink: Id
)
}

View file

@ -1,10 +1,8 @@
package com.anytypeio.anytype.domain.page
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.MainConfig
import com.anytypeio.anytype.domain.page.CloseBlock.Params
/**
* Use-case for closing a smart block by id.
@ -12,22 +10,7 @@ import com.anytypeio.anytype.domain.page.CloseBlock.Params
*/
open class CloseBlock(
private val repo: BlockRepository
) : BaseUseCase<Unit, Params>() {
) : ResultInteractor<Id, Unit>() {
override suspend fun run(params: Params) = try {
repo.closePage(params.id).let {
Either.Right(it)
}
} catch (t: Throwable) {
Either.Left(t)
}
/**
* @property id page's id
*/
data class Params(val id: String) {
companion object {
fun reference() = Params(id = MainConfig.REFERENCE_PAGE_ID)
}
}
override suspend fun doWork(params: Id) = repo.closePage(params)
}

View file

@ -1,24 +1,20 @@
package com.anytypeio.anytype.domain.page
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Result
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.Result
import com.anytypeio.anytype.domain.base.ResultInteractor
open class OpenPage(
private val repo: BlockRepository,
private val auth: AuthRepository
) : BaseUseCase<Result<Payload>, OpenPage.Params>() {
) : ResultInteractor<Id, Result<Payload>>() {
override suspend fun run(params: Params) = safe {
repo.openPage(params.id).also {
auth.saveLastOpenedObjectId(params.id)
override suspend fun doWork(params: Id): Result<Payload> {
return repo.openPage(params).also {
auth.saveLastOpenedObjectId(params)
}
}
/**
* @property id page's id
*/
data class Params(val id: String)
}

View file

@ -21,5 +21,6 @@ sealed class Action {
data class SetUnsplashImage(val img: Id) : Action()
object SearchOnPage: Action()
object UndoRedo : Action()
data class OpenObject(val id: Id) : Action()
data class Duplicate(val id: Id) : Action()
}

View file

@ -158,6 +158,7 @@ import com.anytypeio.anytype.presentation.editor.template.SelectTemplateEvent
import com.anytypeio.anytype.presentation.editor.template.SelectTemplateState
import com.anytypeio.anytype.presentation.editor.template.SelectTemplateViewState
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.extension.getProperObjectName
import com.anytypeio.anytype.presentation.extension.sendAnalyticsBlockActionEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsBlockAlignEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsBlockBackgroundEvent
@ -368,9 +369,10 @@ class EditorViewModel(
is Action.SetUnsplashImage -> {
proceedWithSettingUnsplashImage(action)
}
is Action.Duplicate -> proceedWithOpeningPage(action.id)
is Action.Duplicate -> proceedWithOpeningObject(action.id)
Action.SearchOnPage -> onEnterSearchModeClicked()
Action.UndoRedo -> onUndoRedoActionClicked()
is Action.OpenObject -> proceedWithOpeningObject(action.id)
}
}
}
@ -879,8 +881,8 @@ class EditorViewModel(
}
val startTime = System.currentTimeMillis()
viewModelScope.launch {
openPage(OpenPage.Params(id)).proceed(
success = { result ->
openPage.execute(id).fold(
onSuccess = { result ->
when (result) {
is Result.Success -> {
val middleTime = System.currentTimeMillis()
@ -918,7 +920,7 @@ class EditorViewModel(
}
}
},
failure = {
onFailure = {
session.value = Session.ERROR
Timber.e(it, "Error while opening page with id: $id")
}
@ -1039,11 +1041,9 @@ class EditorViewModel(
Session.IDLE -> navigate(EventWrapper(AppNavigation.Command.Exit))
Session.OPEN -> {
viewModelScope.launch {
closePage(
CloseBlock.Params(context)
).proceed(
success = { navigation.postValue(EventWrapper(AppNavigation.Command.Exit)) },
failure = { Timber.e(it, "Error while closing document: $context") }
closePage.execute(context).fold(
onSuccess = { navigation.postValue(EventWrapper(AppNavigation.Command.Exit)) },
onFailure = { Timber.e(it, "Error while closing document: $context") }
)
}
}
@ -1056,9 +1056,9 @@ class EditorViewModel(
private fun exitDashboard() {
viewModelScope.launch {
closePage(CloseBlock.Params(context)).proceed(
success = { navigateToDesktop() },
failure = { Timber.e(it, "Error while closing this page: $context") }
closePage.execute(context).fold(
onSuccess = { navigateToDesktop() },
onFailure = { Timber.e(it, "Error while closing this page: $context") }
)
}
}
@ -1347,7 +1347,9 @@ class EditorViewModel(
command = Command.OpenDocumentMenu(
isArchived = details[context]?.isArchived ?: false,
isFavorite = details[context]?.isFavorite ?: false,
isLocked = mode == EditorMode.Locked
isLocked = mode == EditorMode.Locked,
fromName = ObjectWrapper.Basic(details[context]?.map ?: emptyMap())
.getProperObjectName() ?: ""
)
)
}
@ -1358,7 +1360,9 @@ class EditorViewModel(
command = Command.OpenDocumentMenu(
isArchived = details[context]?.isArchived ?: false,
isFavorite = details[context]?.isFavorite ?: false,
isLocked = mode == EditorMode.Locked
isLocked = mode == EditorMode.Locked,
fromName = ObjectWrapper.Basic(details[context]?.map ?: emptyMap())
.getProperObjectName() ?: ""
)
)
}
@ -2937,7 +2941,7 @@ class EditorViewModel(
ObjectType.Layout.TODO,
ObjectType.Layout.FILE,
ObjectType.Layout.BOOKMARK -> {
proceedWithOpeningPage(target = target)
proceedWithOpeningObject(target = target)
}
ObjectType.Layout.SET -> {
proceedWithOpeningSet(target = target)
@ -2995,7 +2999,7 @@ class EditorViewModel(
middleTime = middleTime,
context = analyticsContext
)
proceedWithOpeningPage(result.target)
proceedWithOpeningObject(result.target)
}
)
}
@ -3015,7 +3019,7 @@ class EditorViewModel(
jobs += viewModelScope.launch {
createNewObject.execute(Unit).fold(
onSuccess = { id ->
proceedWithOpeningPage(id)
proceedWithOpeningObject(id)
},
onFailure = { e -> Timber.e(e, "Error while creating a new page") }
)
@ -3057,7 +3061,7 @@ class EditorViewModel(
failure = { Timber.e(it, "Error while creating new page with params: $params") },
success = { result ->
orchestrator.proxies.payloads.send(result.payload)
proceedWithOpeningPage(result.target)
proceedWithOpeningObject(result.target)
}
)
}
@ -4088,14 +4092,14 @@ class EditorViewModel(
)
}
fun proceedWithOpeningPage(target: Id) {
fun proceedWithOpeningObject(target: Id) {
viewModelScope.launch {
closePage(CloseBlock.Params(context)).process(
failure = {
closePage.execute(context).fold(
onFailure = {
Timber.e(it, "Error while closing object")
navigate(EventWrapper(AppNavigation.Command.OpenObject(target)))
},
success = {
onSuccess = {
navigate(EventWrapper(AppNavigation.Command.OpenObject(target)))
}
)
@ -4104,8 +4108,8 @@ class EditorViewModel(
fun proceedWithOpeningSet(target: Id, isPopUpToDashboard: Boolean = false) {
viewModelScope.launch {
closePage(CloseBlock.Params(context)).process(
failure = {
closePage.execute(context).fold(
onFailure = {
Timber.e(it, "Error while closing object")
navigate(
EventWrapper(
@ -4116,7 +4120,7 @@ class EditorViewModel(
)
)
},
success = {
onSuccess = {
navigate(
EventWrapper(
AppNavigation.Command.OpenObjectSet(
@ -5301,7 +5305,7 @@ class EditorViewModel(
is Content.Bookmark -> {
val target = content.targetObjectId
if (target != null) {
proceedWithOpeningPage(target)
proceedWithOpeningObject(target)
viewModelScope.sendAnalyticsOpenAsObject(
analytics = analytics,
type = EventsDictionary.Type.bookmark

View file

@ -182,8 +182,8 @@ class ArchiveViewModel(
}
viewModelScope.launch {
openPage(OpenPage.Params(id)).proceed(
success = { result ->
openPage.execute(id).fold(
onSuccess = { result ->
when (result) {
is Result.Success -> {
orchestrator.proxies.payloads.send(result.data)
@ -194,7 +194,7 @@ class ArchiveViewModel(
}
}
},
failure = { Timber.e(it, "Error while opening page with id: $id") }
onFailure = { Timber.e(it, "Error while opening page with id: $id") }
)
}
}
@ -258,12 +258,11 @@ class ArchiveViewModel(
}
private fun proceedWithExitingToDesktop() {
closePage(viewModelScope, CloseBlock.Params(context)) { result ->
result.either(
fnR = {
navigation.postValue(EventWrapper(AppNavigation.Command.ExitToDesktop))
},
fnL = { Timber.e(it, "Error while closing document: $context") }
viewModelScope.launch {
closePage.execute(context).fold(onSuccess = {
navigation.postValue(EventWrapper(AppNavigation.Command.ExitToDesktop))
},
onFailure = { Timber.e(it, "Error while closing document: $context") }
)
}
}

View file

@ -55,7 +55,8 @@ sealed class Command {
data class OpenDocumentMenu(
val isArchived: Boolean,
val isFavorite: Boolean,
val isLocked: Boolean
val isLocked: Boolean,
val fromName: String
) : Command()
data class OpenProfileMenu(

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.presentation.editor.editor
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.event.EventAnalytics
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.domain.base.suspendFold
import com.anytypeio.anytype.domain.block.UpdateDivider
import com.anytypeio.anytype.domain.block.interactor.CreateBlock
import com.anytypeio.anytype.domain.block.interactor.DuplicateBlock
@ -35,7 +36,6 @@ import com.anytypeio.anytype.domain.page.bookmark.CreateBookmarkBlock
import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.presentation.dashboard.HomeDashboardStateMachine
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsChangeTextBlockStyleEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsCopyBlockEvent
@ -98,16 +98,15 @@ class Orchestrator(
when (intent) {
is Intent.CRUD.Create -> {
val startTime = System.currentTimeMillis()
createBlock(
createBlock.execute(
params = CreateBlock.Params(
context = intent.context,
target = intent.target,
prototype = intent.prototype,
position = intent.position
)
).proceed(
failure = defaultOnError,
success = { (id, payload) ->
).suspendFold(
onSuccess = { (id, payload) ->
val middlewareTime = System.currentTimeMillis()
stores.focus.update(Focus.id(id = id))
proxies.payloads.send(payload)
@ -117,7 +116,8 @@ class Orchestrator(
startTime = startTime,
middlewareTime = middlewareTime
)
}
},
onFailure = defaultOnError
)
}
is Intent.CRUD.Replace -> {
@ -443,7 +443,7 @@ class Orchestrator(
hash = intent.hash,
name = intent.name
)
).fold(
).suspendFold(
onSuccess = { uri ->
intent.onDownloaded(uri)
analytics.sendAnalyticsDownloadMediaEvent(intent.type)

View file

@ -363,6 +363,15 @@ fun CoroutineScope.sendAnalyticsRemoveFromFavoritesEvent(
)
}
fun CoroutineScope.sendAnalyticsObjectLinkToEvent(
analytics: Analytics
) {
sendEvent(
analytics = analytics,
eventName = EventsDictionary.objectLinkTo
)
}
fun CoroutineScope.sendAnalyticsMoveToBinEvent(
analytics: Analytics
) {

View file

@ -11,5 +11,6 @@ enum class ObjectAction {
UNDO_REDO,
DUPLICATE,
LOCK,
UNLOCK
UNLOCK,
LINK_TO,
}

View file

@ -14,7 +14,9 @@ import com.anytypeio.anytype.domain.`object`.DuplicateObject
import com.anytypeio.anytype.domain.block.interactor.UpdateFields
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.domain.page.AddBackLinkToObject
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.editor.Editor
@ -27,17 +29,22 @@ class ObjectMenuViewModel(
setObjectIsArchived: SetObjectIsArchived,
addToFavorite: AddToFavorite,
removeFromFavorite: RemoveFromFavorite,
addBackLinkToObject: AddBackLinkToObject,
delegator: Delegator<Action>,
urlBuilder: UrlBuilder,
dispatcher: Dispatcher<Payload>,
menuOptionsProvider: ObjectMenuOptionsProvider,
private val duplicateObject: DuplicateObject,
private val storage: Editor.Storage,
private val analytics: Analytics,
private val updateFields: UpdateFields,
private val delegator: Delegator<Action>
private val updateFields: UpdateFields
) : ObjectMenuViewModelBase(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
addBackLinkToObject = addBackLinkToObject,
delegator = delegator,
urlBuilder = urlBuilder,
dispatcher = dispatcher,
analytics = analytics,
menuOptionsProvider = menuOptionsProvider
@ -67,6 +74,7 @@ class ObjectMenuViewModel(
if (!isProfile) {
add(ObjectAction.DUPLICATE)
}
add(ObjectAction.LINK_TO)
val root = storage.document.get().find { it.id == ctx }
if (root != null) {
@ -160,6 +168,9 @@ class ObjectMenuViewModel(
ObjectAction.REMOVE_FROM_FAVOURITE -> {
proceedWithRemovingFromFavorites(ctx)
}
ObjectAction.LINK_TO -> {
proceedWithLinkTo()
}
ObjectAction.UNLOCK -> {
proceedWithUpdatingLockStatus(ctx, false)
}
@ -260,6 +271,8 @@ class ObjectMenuViewModel(
private val duplicateObject: DuplicateObject,
private val addToFavorite: AddToFavorite,
private val removeFromFavorite: RemoveFromFavorite,
private val addBackLinkToObject: AddBackLinkToObject,
private val urlBuilder: UrlBuilder,
private val storage: Editor.Storage,
private val analytics: Analytics,
private val dispatcher: Dispatcher<Payload>,
@ -273,6 +286,8 @@ class ObjectMenuViewModel(
duplicateObject = duplicateObject,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
addBackLinkToObject = addBackLinkToObject,
urlBuilder = urlBuilder,
storage = storage,
analytics = analytics,
dispatcher = dispatcher,

View file

@ -6,12 +6,19 @@ import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.domain.page.AddBackLinkToObject
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.extension.sendAnalyticsAddToFavoritesEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsMoveToBinEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectLinkToEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRemoveFromFavoritesEvent
import com.anytypeio.anytype.presentation.objects.ObjectAction
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.getProperName
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@ -24,6 +31,9 @@ abstract class ObjectMenuViewModelBase(
private val setObjectIsArchived: SetObjectIsArchived,
private val addToFavorite: AddToFavorite,
private val removeFromFavorite: RemoveFromFavorite,
private val addBackLinkToObject: AddBackLinkToObject,
protected val delegator: Delegator<Action>,
private val urlBuilder: UrlBuilder,
protected val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics,
private val menuOptionsProvider: ObjectMenuOptionsProvider,
@ -149,6 +159,36 @@ abstract class ObjectMenuViewModelBase(
}
}
fun onLinkedMyselfTo(myself: Id, addTo: Id, fromName: String?) {
jobs += viewModelScope.launch {
addBackLinkToObject.execute(
AddBackLinkToObject.Params(objectToLink = myself, objectToPlaceLink = addTo)
).fold(
onSuccess = { obj ->
sendAnalyticsObjectLinkToEvent(analytics)
commands.emit(Command.OpenSnackbar(
id = addTo,
currentObjectName = fromName,
targetObjectName = obj.getProperName(),
icon = ObjectIcon.from(obj, obj.layout, urlBuilder)))
},
onFailure = {
Timber.e(it, "Error while adding link from object to object")
}
)
}
}
protected fun proceedWithLinkTo() {
jobs += viewModelScope.launch { commands.emit(Command.OpenLinkToChooser) }
}
fun proceedWithOpeningPage(id: Id) {
viewModelScope.launch {
delegator.delegate(Action.OpenObject(id))
}
}
sealed class Command {
object OpenObjectIcons : Command()
object OpenSetIcons : Command()
@ -158,6 +198,13 @@ abstract class ObjectMenuViewModelBase(
object OpenSetLayout : Command()
object OpenObjectRelations : Command()
object OpenSetRelations : Command()
object OpenLinkToChooser : Command()
class OpenSnackbar(
val id: Id,
val currentObjectName: String?,
val targetObjectName: String?,
val icon: ObjectIcon
) : Command()
}
companion object {
@ -172,5 +219,4 @@ abstract class ObjectMenuViewModelBase(
const val OBJECT_IS_UNLOCKED_MSG = "Your object is locked"
const val SOMETHING_WENT_WRONG_MSG = "Something went wrong. Please, try again later."
}
}
}

View file

@ -9,7 +9,11 @@ import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.domain.page.AddBackLinkToObject
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.objects.ObjectAction
import com.anytypeio.anytype.presentation.sets.ObjectSet
import com.anytypeio.anytype.presentation.util.Dispatcher
@ -20,6 +24,9 @@ class ObjectSetMenuViewModel(
setObjectIsArchived: SetObjectIsArchived,
addToFavorite: AddToFavorite,
removeFromFavorite: RemoveFromFavorite,
addBackLinkToObject: AddBackLinkToObject,
delegator: Delegator<Action>,
urlBuilder: UrlBuilder,
dispatcher: Dispatcher<Payload>,
state: StateFlow<ObjectSet>,
menuOptionsProvider: ObjectMenuOptionsProvider,
@ -28,6 +35,9 @@ class ObjectSetMenuViewModel(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
addBackLinkToObject = addBackLinkToObject,
delegator = delegator,
urlBuilder = urlBuilder,
dispatcher = dispatcher,
analytics = analytics,
menuOptionsProvider = menuOptionsProvider,
@ -40,6 +50,9 @@ class ObjectSetMenuViewModel(
private val setObjectIsArchived: SetObjectIsArchived,
private val addToFavorite: AddToFavorite,
private val removeFromFavorite: RemoveFromFavorite,
private val addBackLinkToObject: AddBackLinkToObject,
private val delegator: Delegator<Action>,
private val urlBuilder: UrlBuilder,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics,
private val state: StateFlow<ObjectSet>,
@ -50,6 +63,9 @@ class ObjectSetMenuViewModel(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
addBackLinkToObject = addBackLinkToObject,
delegator = delegator,
urlBuilder = urlBuilder,
analytics = analytics,
state = state,
dispatcher = dispatcher,
@ -114,6 +130,7 @@ class ObjectSetMenuViewModel(
} else {
add(ObjectAction.ADD_TO_FAVOURITE)
}
add(ObjectAction.LINK_TO)
}
override fun onActionClicked(ctx: Id, action: ObjectAction) {
@ -130,6 +147,9 @@ class ObjectSetMenuViewModel(
ObjectAction.REMOVE_FROM_FAVOURITE -> {
proceedWithRemovingFromFavorites(ctx)
}
ObjectAction.LINK_TO -> {
proceedWithLinkTo()
}
ObjectAction.MOVE_TO,
ObjectAction.SEARCH_ON_PAGE,
ObjectAction.UNDO_REDO,

View file

@ -2,12 +2,10 @@ package com.anytypeio.anytype.presentation.search
import com.anytypeio.anytype.core_models.*
import com.anytypeio.anytype.core_models.ObjectType.Companion.AUDIO_URL
import com.anytypeio.anytype.core_models.ObjectType.Companion.BOOKMARK_TYPE
import com.anytypeio.anytype.core_models.ObjectType.Companion.DASHBOARD_TYPE
import com.anytypeio.anytype.core_models.ObjectType.Companion.FILE_URL
import com.anytypeio.anytype.core_models.ObjectType.Companion.IMAGE_URL
import com.anytypeio.anytype.core_models.ObjectType.Companion.OBJECT_TYPE_URL
import com.anytypeio.anytype.core_models.ObjectType.Companion.PROFILE_URL
import com.anytypeio.anytype.core_models.ObjectType.Companion.RELATION_URL
import com.anytypeio.anytype.core_models.ObjectType.Companion.TEMPLATE_URL
import com.anytypeio.anytype.core_models.ObjectType.Companion.VIDEO_URL

View file

@ -280,6 +280,7 @@ class ObjectSetViewModel(
is Action.SetUnsplashImage -> {
proceedWithSettingUnsplashImage(action)
}
is Action.OpenObject -> proceedWithOpeningObject(action.id)
else -> {}
}
}
@ -419,9 +420,9 @@ class ObjectSetViewModel(
}
private suspend fun proceedWithClosingAndExit() {
closeBlock(CloseBlock.Params(context)).process(
success = { dispatch(AppNavigation.Command.Exit) },
failure = {
closeBlock.execute(context).fold(
onSuccess = { dispatch(AppNavigation.Command.Exit) },
onFailure = {
Timber.e(it, "Error while closing object set: $context").also {
dispatch(AppNavigation.Command.Exit)
}
@ -1033,13 +1034,13 @@ class ObjectSetViewModel(
//region NAVIGATION
private fun proceedWithOpeningPage(target: Id) {
private fun proceedWithOpeningObject(target: Id) {
jobs += viewModelScope.launch {
closeBlock(CloseBlock.Params(context)).process(
success = {
closeBlock.execute(context).fold(
onSuccess = {
navigate(EventWrapper(AppNavigation.Command.OpenObject(id = target)))
},
failure = {
onFailure = {
Timber.e(it, "Error while closing object set: $context")
navigate(EventWrapper(AppNavigation.Command.OpenObject(id = target)))
}
@ -1055,14 +1056,14 @@ class ObjectSetViewModel(
ObjectType.Layout.NOTE,
ObjectType.Layout.IMAGE,
ObjectType.Layout.FILE,
ObjectType.Layout.BOOKMARK -> proceedWithOpeningPage(target)
ObjectType.Layout.BOOKMARK -> proceedWithOpeningObject(target)
ObjectType.Layout.SET -> {
viewModelScope.launch {
closeBlock(CloseBlock.Params(context)).process(
success = {
closeBlock.execute(context).fold(
onSuccess = {
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(target)))
},
failure = {
onFailure = {
Timber.e(it, "Error while closing object set: $context")
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(target)))
}
@ -1090,9 +1091,9 @@ class ObjectSetViewModel(
fun onHomeButtonClicked() {
viewModelScope.launch {
closeBlock(CloseBlock.Params(context)).process(
success = { dispatch(AppNavigation.Command.ExitToDesktop) },
failure = {
closeBlock.execute(context).fold(
onSuccess = { dispatch(AppNavigation.Command.ExitToDesktop) },
onFailure = {
Timber.e(it, "Error while closing object set: $context").also {
dispatch(AppNavigation.Command.ExitToDesktop)
}
@ -1116,7 +1117,7 @@ class ObjectSetViewModel(
jobs += viewModelScope.launch {
createNewObject.execute(Unit).fold(
onSuccess = { id ->
proceedWithOpeningPage(id)
proceedWithOpeningObject(id)
},
onFailure = { e -> Timber.e(e, "Error while creating a new page") }
)
@ -1125,9 +1126,9 @@ class ObjectSetViewModel(
fun onSearchButtonClicked() {
viewModelScope.launch {
closeBlock(CloseBlock.Params(context)).process(
success = { dispatch(AppNavigation.Command.OpenPageSearch) },
failure = { Timber.e(it, "Error while closing object set: $context") }
closeBlock.execute(context).fold(
onSuccess = { dispatch(AppNavigation.Command.OpenPageSearch) },
onFailure = { Timber.e(it, "Error while closing object set: $context") }
)
}
}

View file

@ -136,6 +136,7 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
@ -387,7 +388,7 @@ open class EditorViewModelTest {
@Test
fun `should start opening page when requested`() {
val param = OpenPage.Params(id = root)
val param = root
stubInterceptEvents()
givenViewModel()
@ -395,7 +396,7 @@ open class EditorViewModelTest {
vm.onStart(root)
runBlockingTest { verify(openPage, times(1)).invoke(param) }
runBlockingTest { verify(openPage, times(1)).execute(param) }
}
@Test
@ -478,17 +479,14 @@ open class EditorViewModelTest {
vm.onSystemBackPressed(editorHasChildrenScreens = false)
runBlockingTest {
verify(closePage, times(1)).invoke(any())
verify(closePage, times(1)).execute(any())
}
}
@Test
fun `should emit an approprtiate navigation command when the page is closed`() {
val response = Either.Right(Unit)
stubInterceptEvents()
stubClosePage(response)
stubClosePage()
givenViewModel()
val testObserver = vm.navigation.test()
@ -509,10 +507,8 @@ open class EditorViewModelTest {
val error = Exception("Error while closing this page")
val response = Either.Left(error)
stubOpenPage(root)
stubClosePage(response)
stubClosePage(error)
stubInterceptEvents()
givenViewModel()
@ -755,7 +751,7 @@ open class EditorViewModelTest {
vm.onAddTextBlockClicked(style = Block.Content.Text.Style.P)
runBlockingTest {
verify(createBlock, times(1)).invoke(any())
verify(createBlock, times(1)).execute(any())
}
}
@ -2146,7 +2142,7 @@ open class EditorViewModelTest {
)
runBlockingTest {
verify(createBlock, times(1)).invoke(
verify(createBlock, times(1)).execute(
params = eq(
CreateBlock.Params(
context = root,
@ -2337,7 +2333,7 @@ open class EditorViewModelTest {
)
runBlockingTest {
verify(createBlock, times(1)).invoke(
verify(createBlock, times(1)).execute(
params = eq(
CreateBlock.Params(
context = root,
@ -2403,11 +2399,7 @@ open class EditorViewModelTest {
runBlockingTest {
verify(createBlock, never()).invoke(
scope = any(),
params = any(),
onResult = any()
)
verify(createBlock, never()).execute(params = any())
verify(updateTextStyle, times(1)).invoke(
params = eq(
@ -2919,12 +2911,8 @@ open class EditorViewModelTest {
vm.proceedWithExitingBack()
runBlockingTest {
verify(closePage, times(1)).invoke(
params = eq(
CloseBlock.Params(
id = root
)
)
verify(closePage, times(1)).execute(
params = eq(root)
)
}
}
@ -3753,7 +3741,7 @@ open class EditorViewModelTest {
objectRestrictions: List<ObjectRestriction> = emptyList()
) {
openPage.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
onBlocking { execute(any()) } doAnswer ValueClassAnswer(
Result.Success(
Payload(
context = root,
@ -3774,10 +3762,17 @@ open class EditorViewModelTest {
}
private fun stubClosePage(
response: Either<Throwable, Unit> = Either.Right(Unit)
exception: Exception? = null
) {
closePage.stub {
onBlocking { invoke(any()) } doReturn response
onBlocking { execute(any()) } doAnswer ValueClassAnswer(Unit)
}
exception?.let {
closePage.stub {
onBlocking { execute(any()) } doAnswer { invocationOnMock -> throw exception }
}
}
}
@ -3800,7 +3795,7 @@ open class EditorViewModelTest {
events: List<Event> = emptyList()
) {
openPage.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
onBlocking { execute(any()) } doAnswer ValueClassAnswer(
Result.Success(
Payload(
context = context,
@ -3860,7 +3855,7 @@ open class EditorViewModelTest {
private fun stubCreateBlock(root: String) {
createBlock.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
onBlocking { execute(any()) } doAnswer ValueClassAnswer(
Pair(
MockDataFactory.randomString(), Payload(
context = root,

View file

@ -51,7 +51,6 @@ class EditorBackButtonTest : EditorPresentationTestSetup() {
assertTrue(stateBackPressed?.styleTextToolbar?.isVisible == false)
val params = CloseBlock.Params(id = root)
verifyBlocking(closePage, times(1)) { invoke(params) }
verifyBlocking(closePage, times(1)) { execute(root) }
}
}

View file

@ -89,7 +89,7 @@ class EditorEmptySpaceInteractionTest : EditorPresentationTestSetup() {
vm.onOutsideClicked()
verifyBlocking(createBlock, times(1)) {
invoke(
execute(
params = eq(
CreateBlock.Params(
context = root,
@ -142,7 +142,7 @@ class EditorEmptySpaceInteractionTest : EditorPresentationTestSetup() {
vm.onOutsideClicked()
verifyBlocking(createBlock, times(1)) {
invoke(
execute(
params = eq(
CreateBlock.Params(
context = root,
@ -184,7 +184,7 @@ class EditorEmptySpaceInteractionTest : EditorPresentationTestSetup() {
vm.onOutsideClicked()
verifyBlocking(createBlock, times(1)) {
invoke(
execute(
params = eq(
CreateBlock.Params(
target = "",
@ -335,7 +335,7 @@ class EditorEmptySpaceInteractionTest : EditorPresentationTestSetup() {
vm.onOutsideClicked()
verifyBlocking(createBlock, times(1)) {
invoke(
execute(
params = eq(
CreateBlock.Params(
target = "",

View file

@ -22,6 +22,7 @@ import com.anytypeio.anytype.presentation.editor.render.parseThemeBackgroundColo
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.presentation.util.TXT
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import com.jraska.livedata.test
import kotlinx.coroutines.runBlocking
import net.lachlanmckee.timberjunit.TimberTestRule
@ -32,6 +33,7 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
@ -872,28 +874,29 @@ class EditorMentionTest : EditorPresentationTestSetup() {
val params = InterceptEvents.Params(context = root)
openPage.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
Result.Success(
Payload(
context = root,
events = listOf(
Event.Command.ShowObject(
onBlocking { execute(any()) } doAnswer
ValueClassAnswer(
Result.Success(
Payload(
context = root,
root = root,
details = Block.Details(),
relations = emptyList(),
blocks = document,
objectRestrictions = emptyList()
),
Event.Command.Details.Amend(
context = root,
target = mentionTarget,
details = mapOf(Block.Fields.NAME_KEY to "Foob")
events = listOf(
Event.Command.ShowObject(
context = root,
root = root,
details = Block.Details(),
relations = emptyList(),
blocks = document,
objectRestrictions = emptyList()
),
Event.Command.Details.Amend(
context = root,
target = mentionTarget,
details = mapOf(Block.Fields.NAME_KEY to "Foob")
)
)
)
)
)
)
)
}
stubInterceptEvents()
stubGetObjectTypes()
@ -1019,28 +1022,29 @@ class EditorMentionTest : EditorPresentationTestSetup() {
val params = InterceptEvents.Params(context = root)
openPage.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
Result.Success(
Payload(
context = root,
events = listOf(
Event.Command.ShowObject(
onBlocking { execute(any()) } doAnswer
ValueClassAnswer(
Result.Success(
Payload(
context = root,
root = root,
details = Block.Details(),
relations = emptyList(),
blocks = document,
objectRestrictions = emptyList()
),
Event.Command.Details.Amend(
context = root,
target = mentionTarget,
details = mapOf(Block.Fields.NAME_KEY to "")
events = listOf(
Event.Command.ShowObject(
context = root,
root = root,
details = Block.Details(),
relations = emptyList(),
blocks = document,
objectRestrictions = emptyList()
),
Event.Command.Details.Amend(
context = root,
target = mentionTarget,
details = mapOf(Block.Fields.NAME_KEY to "")
)
)
)
)
)
)
)
}
stubInterceptEvents()
stubGetObjectTypes()

View file

@ -100,7 +100,8 @@ class EditorMenuTest : EditorPresentationTestSetup() {
value.peekContent() == Command.OpenDocumentMenu(
isArchived = false,
isFavorite = false,
isLocked = false
isLocked = false,
fromName = ""
)
}
}
@ -133,7 +134,8 @@ class EditorMenuTest : EditorPresentationTestSetup() {
value.peekContent() == Command.OpenDocumentMenu(
isArchived = false,
isFavorite = false,
isLocked = false
isLocked = false,
fromName = ""
)
}
}

View file

@ -389,23 +389,24 @@ open class EditorPresentationTestSetup {
objectRestrictions: List<ObjectRestriction> = emptyList()
) {
openPage.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
Result.Success(
Payload(
context = root,
events = listOf(
Event.Command.ShowObject(
onBlocking { execute(any()) } doAnswer
ValueClassAnswer(
Result.Success(
Payload(
context = root,
root = root,
details = details,
relations = relations,
blocks = document,
objectRestrictions = objectRestrictions
events = listOf(
Event.Command.ShowObject(
context = root,
root = root,
details = details,
relations = relations,
blocks = document,
objectRestrictions = objectRestrictions
)
)
)
)
)
)
)
}
}
@ -489,14 +490,16 @@ open class EditorPresentationTestSetup {
fun stubCreateBlock(root: String) {
createBlock.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
Pair(
MockDataFactory.randomString(), Payload(
context = root,
events = listOf()
onBlocking { execute(any()) } doAnswer
ValueClassAnswer(
Pair(
MockDataFactory.randomString(), Payload(
context = root,
events = listOf()
)
)
)
)
)
}
}
@ -560,7 +563,7 @@ open class EditorPresentationTestSetup {
fun stubClosePage() {
closePage.stub {
onBlocking { invoke(any()) } doReturn Either.Right(Unit)
onBlocking { execute(any()) } doAnswer ValueClassAnswer(Unit)
}
}

View file

@ -124,7 +124,7 @@ class EditorSlashWidgetRelationsTest: EditorPresentationTestSetup() {
prototype = Block.Prototype.Relation(key = r2.key)
)
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
@Test

View file

@ -277,7 +277,7 @@ class EditorSplitTest : EditorPresentationTestSetup() {
coroutineTestRule.advanceTime(EditorViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verifyBlocking(createBlock, times(1)) {
invoke(
execute(
params = eq(
CreateBlock.Params(
context = root,
@ -450,7 +450,7 @@ class EditorSplitTest : EditorPresentationTestSetup() {
coroutineTestRule.advanceTime(EditorViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verifyBlocking(createBlock, times(1)) {
invoke(
execute(
params = eq(
CreateBlock.Params(
context = root,

View file

@ -117,9 +117,7 @@ class EditorTextUpdateTest : EditorPresentationTestSetup() {
target = block.id
)
)
inOrder.verify(closePage, times(1)).invoke(
CloseBlock.Params(id = root)
)
inOrder.verify(closePage, times(1)).execute(root)
}
// RELEASING PENDING COROUTINES
@ -194,7 +192,7 @@ class EditorTextUpdateTest : EditorPresentationTestSetup() {
vm.onHomeButtonClicked()
verifyBlocking(closePage, times(1)) {
invoke(CloseBlock.Params(id = root))
execute(root)
}
verifyNoMoreInteractions(updateText)
@ -264,8 +262,8 @@ class EditorTextUpdateTest : EditorPresentationTestSetup() {
target = block.id
)
)
inOrder.verify(closePage, times(1)).invoke(
CloseBlock.Params(id = root)
inOrder.verify(closePage, times(1)).execute(
root
)
}
@ -341,7 +339,7 @@ class EditorTextUpdateTest : EditorPresentationTestSetup() {
vm.onSystemBackPressed(false)
verifyBlocking(closePage, times(1)) {
invoke(CloseBlock.Params(id = root))
execute(root)
}
verifyNoMoreInteractions(updateText)

View file

@ -10,10 +10,12 @@ import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.page.CreateDocument
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
@ -93,7 +95,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
onAddTextBlockClicked(style)
}
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
@Test
@ -148,7 +150,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
onAddTextBlockClicked(style)
}
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
@Test
@ -287,7 +289,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
onAddFileBlockClicked(type = type)
}
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
@Test
@ -346,7 +348,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
onAddFileBlockClicked(type = type)
}
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
@Test
@ -387,7 +389,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
onAddBookmarkBlockClicked()
}
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
@Test
@ -439,7 +441,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
onAddBookmarkBlockClicked()
}
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
@Test
@ -480,7 +482,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
onAddDividerBlockClicked(style = Block.Content.Divider.Style.LINE)
}
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
@Test
@ -532,22 +534,23 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
onAddDividerBlockClicked(style = Block.Content.Divider.Style.LINE)
}
verifyBlocking(createBlock, times(1)) { invoke(params) }
verifyBlocking(createBlock, times(1)) { execute(params) }
}
private fun stubCreateBlock(
params: CreateBlock.Params
) {
createBlock.stub {
onBlocking { invoke(params) } doReturn Either.Right(
Pair(
MockDataFactory.randomUuid(),
Payload(
context = root,
events = emptyList()
onBlocking { execute(params) } doAnswer
ValueClassAnswer(
Pair(
MockDataFactory.randomUuid(),
Payload(
context = root,
events = emptyList()
)
)
)
)
)
}
}

View file

@ -435,7 +435,7 @@ class ObjectSetNavigationTest : ObjectSetViewModelTestSetup() {
vm.onGridCellClicked(stateAfterLoaded.rows.first().cells.last())
verifyBlocking(closeBlock, times(1)) {
invoke(CloseBlock.Params(root))
execute(root)
}
}
}

View file

@ -28,41 +28,37 @@ import com.anytypeio.anytype.domain.dataview.SetDataViewSource
import com.anytypeio.anytype.domain.dataview.interactor.AddNewRelationToDataView
import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewRecord
import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer
import com.anytypeio.anytype.domain.event.interactor.EventChannel
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.DefaultObjectStore
import com.anytypeio.anytype.domain.objects.ObjectStore
import com.anytypeio.anytype.domain.page.CloseBlock
import com.anytypeio.anytype.domain.page.CreateNewObject
import com.anytypeio.anytype.domain.search.CancelSearchSubscription
import com.anytypeio.anytype.domain.search.DataViewSubscriptionContainer
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.domain.sets.OpenObjectSet
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.domain.page.CreateNewObject
import com.anytypeio.anytype.domain.search.CancelSearchSubscription
import com.anytypeio.anytype.domain.search.DataViewSubscriptionContainer
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.presentation.sets.ObjectSetDatabase
import com.anytypeio.anytype.presentation.sets.ObjectSetPaginator
import com.anytypeio.anytype.presentation.sets.ObjectSetRecordCache
import com.anytypeio.anytype.presentation.sets.ObjectSetReducer
import com.anytypeio.anytype.presentation.sets.ObjectSetSession
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import org.junit.Rule
import org.mockito.Mock
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
@ -247,7 +243,7 @@ open class ObjectSetViewModelTestSetup {
fun stubCloseBlock() {
closeBlock.stub {
onBlocking { invoke(any()) } doReturn Either.Right(Unit)
onBlocking { execute(any()) } doAnswer ValueClassAnswer(Unit)
}
}