diff --git a/CHANGELOG.md b/CHANGELOG.md index 6947070c2b..bc3dffcc47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixes & tech 🚒 +* Cannot add block after document's title via add-block-menu (#827) * When navigating to a document via search-screen, open this new document without passing by dashboard-screen (#830) * Inconsistent logic when adding markup in certain corner cases (#509) * If you change checkbox's text color and then check off this checkbox, its text color always becomes black whereas it should have the color that you've set before (#785) diff --git a/app/src/main/java/com/agileburo/anytype/navigation/Navigator.kt b/app/src/main/java/com/agileburo/anytype/navigation/Navigator.kt index b6edcda9f1..60a7ffe927 100644 --- a/app/src/main/java/com/agileburo/anytype/navigation/Navigator.kt +++ b/app/src/main/java/com/agileburo/anytype/navigation/Navigator.kt @@ -5,6 +5,7 @@ import androidx.core.os.bundleOf import androidx.navigation.NavController import androidx.navigation.navOptions import com.agileburo.anytype.R +import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.presentation.navigation.AppNavigation import com.agileburo.anytype.presentation.settings.EditorSettings import com.agileburo.anytype.ui.auth.Keys @@ -161,11 +162,12 @@ class Navigator : AppNavigation { navController?.navigate(R.id.pageNavigationFragment, bundle) } - override fun openLinkTo(target: String, context: String, replace: Boolean) { + override fun openLinkTo(target: String, context: String, replace: Boolean, position: Position) { val bundle = bundleOf( LinkToObjectFragment.TARGET_ID_KEY to target, LinkToObjectFragment.CONTEXT_ID_KEY to context, - LinkToObjectFragment.REPLACE_KEY to replace + LinkToObjectFragment.REPLACE_KEY to replace, + LinkToObjectFragment.POSITION_KEY to position.name ) navController?.navigate(R.id.linkToFragment, bundle) } diff --git a/app/src/main/java/com/agileburo/anytype/ui/base/NavigationFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/base/NavigationFragment.kt index afddb91f66..ef3834ddc3 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/base/NavigationFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/base/NavigationFragment.kt @@ -54,7 +54,8 @@ abstract class NavigationFragment( is Command.OpenLinkToScreen -> navigation.openLinkTo( command.target, command.context, - command.replace + command.replace, + command.position ) is Command.OpenMoveToScreen -> navigation.openMoveTo( targets = command.targets, diff --git a/app/src/main/java/com/agileburo/anytype/ui/linking/LinkToObjectFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/linking/LinkToObjectFragment.kt index 1159e7eb52..bf99950489 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/linking/LinkToObjectFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/linking/LinkToObjectFragment.kt @@ -14,6 +14,7 @@ import com.agileburo.anytype.core_ui.layout.State import com.agileburo.anytype.core_utils.ext.* import com.agileburo.anytype.core_utils.ui.ViewState import com.agileburo.anytype.di.common.componentManager +import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.emojifier.Emojifier import com.agileburo.anytype.presentation.linking.LinkToObjectViewModel import com.agileburo.anytype.presentation.linking.LinkToObjectViewModelFactory @@ -35,6 +36,14 @@ import javax.inject.Inject class LinkToObjectFragment : ViewStateFragment>(R.layout.fragment_link_to_object) { + private val position: Position + get() { + val args = requireArguments() + val value = args.getString(POSITION_KEY) + checkNotNull(value) + return Position.valueOf(value) + } + private val replace: Boolean get() = arguments?.getBoolean(REPLACE_KEY) ?: false @@ -121,14 +130,16 @@ class LinkToObjectFragment : vm.onLinkToObjectClicked( context = targetContext, target = target, - replace = replace + replace = replace, + position = position ) } btnLinkToObjectSmall.setOnClickListener { vm.onLinkToObjectClicked( context = targetContext, target = target, - replace = replace + replace = replace, + position = position ) } vm.onStart(targetContext) @@ -216,6 +227,7 @@ class LinkToObjectFragment : companion object { const val TARGET_ID_KEY = "arg.link_to.target" const val REPLACE_KEY = "arg.link_to.replace" + const val POSITION_KEY = "arg.link_to.position" const val CONTEXT_ID_KEY = "arg.link_to.context" const val POSITION_FROM = 0 const val POSITION_TO = 1 diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt index 801734afaf..6d4deac9ab 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -2,7 +2,9 @@ package com.agileburo.anytype.data.auth.repo.block import com.agileburo.anytype.data.auth.mapper.toDomain import com.agileburo.anytype.data.auth.mapper.toEntity +import com.agileburo.anytype.data.auth.model.PositionEntity import com.agileburo.anytype.domain.block.model.Command +import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.domain.block.repo.BlockRepository import com.agileburo.anytype.domain.clipboard.Copy import com.agileburo.anytype.domain.clipboard.Paste @@ -164,11 +166,13 @@ class BlockDataRepository( context: Id, target: Id, block: Id, - replace: Boolean + replace: Boolean, + position: Position ): Payload = factory.remote.linkToObject( context = context, target = target, block = block, - replace = replace + replace = replace, + position = PositionEntity.valueOf(position.name) ).toDomain() } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt index 807aa3472d..06d97ba821 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt @@ -51,6 +51,7 @@ interface BlockDataStore { context: String, target: String, block: String, - replace: Boolean + replace: Boolean, + position: PositionEntity ): PayloadEntity } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt index b9b2da64be..87219d58bd 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt @@ -51,6 +51,7 @@ interface BlockRemote { context: String, target: String, block: String, - replace: Boolean + replace: Boolean, + position: PositionEntity ): PayloadEntity } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt index af4d436c1a..1b811df211 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt @@ -134,11 +134,13 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore { context: String, target: String, block: String, - replace: Boolean + replace: Boolean, + position: PositionEntity ): PayloadEntity = remote.linkToObject( context = context, target = target, block = block, - replace = replace + replace = replace, + position = position ) } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/CreateLinkToObject.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/CreateLinkToObject.kt index fe1e61fd6d..3be9ab6a99 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/CreateLinkToObject.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/CreateLinkToObject.kt @@ -2,6 +2,7 @@ package com.agileburo.anytype.domain.block.interactor import com.agileburo.anytype.domain.base.BaseUseCase import com.agileburo.anytype.domain.block.interactor.CreateLinkToObject.Params +import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.domain.block.repo.BlockRepository import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.event.model.Payload @@ -12,14 +13,15 @@ import com.agileburo.anytype.domain.event.model.Payload */ class CreateLinkToObject( private val repo: BlockRepository -) : BaseUseCase() { +) : BaseUseCase() { override suspend fun run(params: Params) = safe { repo.linkToObject( context = params.context, target = params.target, block = params.block, - replace = params.replace + replace = params.replace, + position = params.position ) } @@ -34,6 +36,7 @@ class CreateLinkToObject( val context: Id, val block: Id, val target: Id, - val replace: Boolean + val replace: Boolean, + val position: Position ) } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt index f52fd98db4..73f2fe870b 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt @@ -1,6 +1,7 @@ package com.agileburo.anytype.domain.block.repo import com.agileburo.anytype.domain.block.model.Command +import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.domain.clipboard.Copy import com.agileburo.anytype.domain.clipboard.Paste import com.agileburo.anytype.domain.common.Hash @@ -95,5 +96,11 @@ interface BlockRepository { suspend fun getPageInfoWithLinks(pageId: String): PageInfoWithLinks suspend fun getListPages(): List - suspend fun linkToObject(context: Id, target: Id, block: Id, replace: Boolean): Payload + suspend fun linkToObject( + context: Id, + target: Id, + block: Id, + replace: Boolean, + position: Position + ): Payload } \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt index d8944ebcbd..87e7087221 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt @@ -158,6 +158,7 @@ class BlockMiddleware( context: String, target: String, block: String, - replace: Boolean - ): PayloadEntity = middleware.linkToObject(context, target, block, replace) + replace: Boolean, + position: PositionEntity + ): PayloadEntity = middleware.linkToObject(context, target, block, replace, position) } \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java index 6dde32cc2a..247a2fa9a3 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java @@ -1050,7 +1050,8 @@ public class Middleware { public PayloadEntity linkToObject( String contextId, String targetId, String blockId, - boolean replace + boolean replace, + PositionEntity positionEntity ) throws Exception { Models.Block.Position position = null; @@ -1058,7 +1059,7 @@ public class Middleware { if (replace) { position = Models.Block.Position.Replace; } else { - position = Models.Block.Position.Bottom; + position = mapper.toMiddleware(positionEntity); } Models.Block.Content.Link link = Models.Block.Content.Link diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/linking/LinkToObjectViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/linking/LinkToObjectViewModel.kt index b295740fa0..a3585d4169 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/linking/LinkToObjectViewModel.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/linking/LinkToObjectViewModel.kt @@ -7,6 +7,7 @@ import com.agileburo.anytype.core_utils.ext.timber import com.agileburo.anytype.core_utils.ui.ViewState import com.agileburo.anytype.core_utils.ui.ViewStateViewModel import com.agileburo.anytype.domain.block.interactor.CreateLinkToObject +import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.config.GetConfig import com.agileburo.anytype.domain.misc.UrlBuilder @@ -93,7 +94,8 @@ class LinkToObjectViewModel( fun onLinkToObjectClicked( context: Id, target: Id, - replace: Boolean + replace: Boolean, + position: Position ) { viewModelScope.launch { createLinkToObject( @@ -101,7 +103,8 @@ class LinkToObjectViewModel( context = context, target = target, block = pageId, - replace = replace + replace = replace, + position = position ) ).proceed( failure = { Timber.e(it, "Error while creating link to object") }, diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/navigation/AppNavigation.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/navigation/AppNavigation.kt index 14b8b8ff19..c1ae1ebaf1 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/navigation/AppNavigation.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/navigation/AppNavigation.kt @@ -1,5 +1,6 @@ package com.agileburo.anytype.presentation.navigation +import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.presentation.settings.EditorSettings interface AppNavigation { @@ -35,8 +36,8 @@ interface AppNavigation { fun exitToDesktop() fun openDebugSettings() fun openPageNavigation(target: String) - fun openLinkTo(target: String, context: String, replace: Boolean) fun openMoveTo(targets: List, context: String) + fun openLinkTo(target: String, context: String, replace: Boolean, position: Position) fun openPageSearch() fun exitToDesktopAndOpenPage(pageId: String) fun exitToInvitationCodeScreen() @@ -82,7 +83,8 @@ interface AppNavigation { data class OpenLinkToScreen( val context: String, val target: String, - val replace: Boolean + val replace: Boolean, + val position: Position ) : Command() data class OpenMoveToScreen( diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt index a70fbcd308..1e3d5771d9 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt @@ -1190,9 +1190,26 @@ class PageViewModel( ) } } else { + + var id = target.id + + val position: Position + + if (target.id == context) { + if (target.children.isEmpty()) + position = Position.INNER + else { + position = Position.TOP + id = target.children.first() + } + } else { + position = Position.BOTTOM + } + proceedWithCreatingNewTextBlock( - id = orchestrator.stores.focus.current().id, - style = style + id = id, + style = style, + position = position ) } @@ -1211,14 +1228,25 @@ class PageViewModel( fun onAddLinkToObjectClicked() { - val target = orchestrator.stores.focus.current().id + val focused = blocks.first { it.id == orchestrator.stores.focus.current().id } - val block = blocks.first { it.id == target } - - val content = block.content + val content = focused.content val replace = content is Content.Text && content.text.isEmpty() + var position : Position = Position.BOTTOM + + var target: Id = focused.id + + if (!replace && focused.id == context) { + if (focused.children.isEmpty()) { + position = Position.INNER + } else { + position = Position.TOP + target = focused.children.first() + } + } + proceedWithClearingFocus() navigate( @@ -1226,7 +1254,8 @@ class PageViewModel( AppNavigation.Command.OpenLinkToScreen( target = target, context = context, - replace = replace + replace = replace, + position = position ) ) ) @@ -1258,18 +1287,37 @@ class PageViewModel( } fun onAddFileBlockClicked(type: Content.File.Type) { - val target = blocks.first { it.id == orchestrator.stores.focus.current().id } - val content = target.content + + val focused = blocks.first { it.id == orchestrator.stores.focus.current().id } + + val content = focused.content if (content is Content.Text && content.text.isEmpty()) { proceedWithReplacingByEmptyFileBlock( - id = target.id, + id = focused.id, type = type ) } else { + + val position : Position + + var target: Id = focused.id + + if (focused.id == context) { + if (focused.children.isEmpty()) { + position = Position.INNER + } else { + position = Position.TOP + target = focused.children.first() + } + } else { + position = Position.BOTTOM + } + proceedWithCreatingEmptyFileBlock( - id = target.id, - type = type + id = target, + type = type, + position = position ) } } @@ -1581,26 +1629,43 @@ class PageViewModel( } fun onAddDividerBlockClicked() { - val target = blocks.first { it.id == orchestrator.stores.focus.current().id } - val content = target.content + + val focused = blocks.first { it.id == orchestrator.stores.focus.current().id } + val content = focused.content if (content is Content.Text && content.text.isEmpty()) { viewModelScope.launch { orchestrator.proxies.intents.send( Intent.CRUD.Replace( context = context, - target = target.id, + target = focused.id, prototype = Prototype.Divider ) ) } } else { + + val position : Position + + var target: Id = focused.id + + if (focused.id == context) { + if (focused.children.isEmpty()) { + position = Position.INNER + } else { + position = Position.TOP + target = focused.children.first() + } + } else { + position = Position.BOTTOM + } + viewModelScope.launch { orchestrator.proxies.intents.send( Intent.CRUD.Create( context = context, - target = target.id, - position = Position.BOTTOM, + target = target, + position = position, prototype = Prototype.Divider ) ) @@ -1689,10 +1754,27 @@ class PageViewModel( fun onAddNewPageClicked() { controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected) + val position : Position + + val focused = blocks.first { it.id == orchestrator.stores.focus.current().id } + + var target = focused.id + + if (focused.id == context) { + if (focused.children.isEmpty()) + position = Position.INNER + else { + position = Position.TOP + target = focused.children.first() + } + } else { + position = Position.BOTTOM + } + val params = CreateDocument.Params( context = context, - position = Position.BOTTOM, - target = orchestrator.stores.focus.current().id, + position = position, + target = target, prototype = Prototype.Page(style = Content.Page.Style.EMPTY) ) @@ -1725,26 +1807,44 @@ class PageViewModel( } fun onAddBookmarkBlockClicked() { - val target = blocks.first { it.id == orchestrator.stores.focus.current().id } - val content = target.content + + val focused = blocks.first { it.id == orchestrator.stores.focus.current().id } + + val content = focused.content if (content is Content.Text && content.text.isEmpty()) { viewModelScope.launch { orchestrator.proxies.intents.send( Intent.CRUD.Replace( context = context, - target = target.id, + target = focused.id, prototype = Prototype.Bookmark ) ) } } else { + + val position : Position + + var target: Id = focused.id + + if (focused.id == context) { + if (focused.children.isEmpty()) { + position = Position.INNER + } else { + position = Position.TOP + target = focused.children.first() + } + } else { + position = Position.BOTTOM + } + viewModelScope.launch { orchestrator.proxies.intents.send( Intent.CRUD.Create( context = context, - position = Position.BOTTOM, - target = target.id, + position = position, + target = target, prototype = Prototype.Bookmark ) ) diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/page/editor/EditorTitleAddBlockTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/page/editor/EditorTitleAddBlockTest.kt new file mode 100644 index 0000000000..235b061491 --- /dev/null +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/page/editor/EditorTitleAddBlockTest.kt @@ -0,0 +1,576 @@ +package com.agileburo.anytype.presentation.page.editor + +import MockDataFactory +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.agileburo.anytype.domain.base.Either +import com.agileburo.anytype.domain.block.interactor.CreateBlock +import com.agileburo.anytype.domain.block.model.Block +import com.agileburo.anytype.domain.block.model.Position +import com.agileburo.anytype.domain.event.interactor.InterceptEvents +import com.agileburo.anytype.domain.event.model.Payload +import com.agileburo.anytype.domain.page.CreateDocument +import com.agileburo.anytype.presentation.util.CoroutinesTestRule +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.stub +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verifyBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.MockitoAnnotations + +class EditorTitleAddBlockTest : EditorPresentationTestSetup() { + + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun `should create a new text block after title with INNER position if document has only title block`() { + + // SETUP + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf() + ) + + val style = Block.Content.Text.Style.values().random() + + val params = CreateBlock.Params( + context = root, + target = root, + position = Position.INNER, + prototype = Block.Prototype.Text(style) + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateBlock(params) + + val vm = buildViewModel() + + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddTextBlockClicked(style) + } + + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + @Test + fun `should create a new text block at the TOP of the document's first block`() { + + // SETUP + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields(emptyMap()), + content = Block.Content.Text( + text = MockDataFactory.randomString(), + marks = emptyList(), + style = Block.Content.Text.Style.values().random() + ), + children = emptyList() + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(block.id) + ) + + val style = Block.Content.Text.Style.values().random() + + val params = CreateBlock.Params( + context = root, + target = block.id, + position = Position.TOP, + prototype = Block.Prototype.Text(style) + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateBlock(params) + + val vm = buildViewModel() + + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddTextBlockClicked(style) + } + + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + @Test + fun `should create a new document after title with INNER position if document has only title block`() { + + // SETUP + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf() + ) + + val params = CreateDocument.Params( + context = root, + target = root, + position = Position.INNER, + prototype = Block.Prototype.Page( + style = Block.Content.Page.Style.EMPTY + ) + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateDocument(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddNewPageClicked() + } + + verifyBlocking(createDocument, times(1)) { invoke(params) } + } + + @Test + fun `should create a new document at the TOP of the document's first block`() { + + // SETUP + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields(emptyMap()), + content = Block.Content.Text( + text = MockDataFactory.randomString(), + marks = emptyList(), + style = Block.Content.Text.Style.values().random() + ), + children = emptyList() + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(block.id) + ) + + val params = CreateDocument.Params( + context = root, + target = block.id, + position = Position.TOP, + prototype = Block.Prototype.Page( + style = Block.Content.Page.Style.EMPTY + ) + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateDocument(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddNewPageClicked() + } + + verifyBlocking(createDocument, times(1)) { invoke(params) } + } + + @Test + fun `should create a new file block after title with INNER position if document has only title block`() { + + // SETUP + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf() + ) + + val types = Block.Content.File.Type.values().filter { it != Block.Content.File.Type.NONE } + + val type = types.random() + + val params = CreateBlock.Params( + context = root, + target = root, + position = Position.INNER, + prototype = Block.Prototype.File( + type = type, + state = Block.Content.File.State.EMPTY + ) + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateBlock(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddFileBlockClicked(type = type) + } + + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + @Test + fun `should create a new file block at the TOP of the document's first block`() { + + // SETUP + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields(emptyMap()), + content = Block.Content.Text( + text = MockDataFactory.randomString(), + marks = emptyList(), + style = Block.Content.Text.Style.values().random() + ), + children = emptyList() + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(block.id) + ) + + val types = Block.Content.File.Type.values().filter { it != Block.Content.File.Type.NONE } + + val type = types.random() + + val params = CreateBlock.Params( + context = root, + target = block.id, + position = Position.TOP, + prototype = Block.Prototype.File( + type = type, + state = Block.Content.File.State.EMPTY + ) + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateBlock(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddFileBlockClicked(type = type) + } + + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + @Test + fun `should create a new bookmark block after title with INNER position if document has only title block`() { + + // SETUP + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf() + ) + + val params = CreateBlock.Params( + context = root, + target = root, + position = Position.INNER, + prototype = Block.Prototype.Bookmark + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateBlock(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddBookmarkBlockClicked() + } + + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + @Test + fun `should create a new bookmark block at the TOP of the document's first block`() { + + // SETUP + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields(emptyMap()), + content = Block.Content.Text( + text = MockDataFactory.randomString(), + marks = emptyList(), + style = Block.Content.Text.Style.values().random() + ), + children = emptyList() + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(block.id) + ) + + val params = CreateBlock.Params( + context = root, + target = block.id, + position = Position.TOP, + prototype = Block.Prototype.Bookmark + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateBlock(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddBookmarkBlockClicked() + } + + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + @Test + fun `should create a new divider block after title with INNER position if document has only title block`() { + + // SETUP + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf() + ) + + val params = CreateBlock.Params( + context = root, + target = root, + position = Position.INNER, + prototype = Block.Prototype.Divider + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateBlock(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddDividerBlockClicked() + } + + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + @Test + fun `should create a new divider block at the TOP of the document's first block`() { + + // SETUP + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields(emptyMap()), + content = Block.Content.Text( + text = MockDataFactory.randomString(), + marks = emptyList(), + style = Block.Content.Text.Style.values().random() + ), + children = emptyList() + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(block.id) + ) + + val params = CreateBlock.Params( + context = root, + target = block.id, + position = Position.TOP, + prototype = Block.Prototype.Divider + ) + + val document = listOf(page) + + stubOpenDocument(document = document) + stubInterceptEvents(InterceptEvents.Params(context = root)) + stubCreateBlock(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged( + id = root, + hasFocus = true + ) + onAddDividerBlockClicked() + } + + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + private fun stubCreateBlock( + params: CreateBlock.Params + ) { + createBlock.stub { + onBlocking { invoke(params) } doReturn Either.Right( + Pair( + MockDataFactory.randomUuid(), + Payload( + context = root, + events = emptyList() + ) + ) + ) + } + } + + private fun stubCreateDocument( + params: CreateDocument.Params + ) { + createDocument.stub { + onBlocking { invoke(params) } doReturn Either.Right( + CreateDocument.Result( + id = MockDataFactory.randomUuid(), + payload = Payload( + context = root, + events = emptyList() + ), + target = MockDataFactory.randomUuid() + ) + ) + } + } +} \ No newline at end of file