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:
parent
68764be5e9
commit
8cf9117cb6
44 changed files with 546 additions and 269 deletions
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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..."
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 -> {
|
||||
|
||||
}
|
||||
|
|
10
core-ui/src/main/res/drawable/ic_object_action_link_to.xml
Normal file
10
core-ui/src/main/res/drawable/ic_object_action_link_to.xml
Normal 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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -363,6 +363,15 @@ fun CoroutineScope.sendAnalyticsRemoveFromFavoritesEvent(
|
|||
)
|
||||
}
|
||||
|
||||
fun CoroutineScope.sendAnalyticsObjectLinkToEvent(
|
||||
analytics: Analytics
|
||||
) {
|
||||
sendEvent(
|
||||
analytics = analytics,
|
||||
eventName = EventsDictionary.objectLinkTo
|
||||
)
|
||||
}
|
||||
|
||||
fun CoroutineScope.sendAnalyticsMoveToBinEvent(
|
||||
analytics: Analytics
|
||||
) {
|
||||
|
|
|
@ -11,5 +11,6 @@ enum class ObjectAction {
|
|||
UNDO_REDO,
|
||||
DUPLICATE,
|
||||
LOCK,
|
||||
UNLOCK
|
||||
UNLOCK,
|
||||
LINK_TO,
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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 = "",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -435,7 +435,7 @@ class ObjectSetNavigationTest : ObjectSetViewModelTestSetup() {
|
|||
vm.onGridCellClicked(stateAfterLoaded.rows.first().cells.last())
|
||||
|
||||
verifyBlocking(closeBlock, times(1)) {
|
||||
invoke(CloseBlock.Params(root))
|
||||
execute(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue