diff --git a/CHANGELOG.md b/CHANGELOG.md index 4466249d1c..36ed522a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes & tech 🚒 +* When adding new block via add-block screen, should replace current text block instead of adding a new block after this text block if this text block is empty (#325) * Divider block should be selectable in multi-select and scroll-and-move mode (#778) ## Version 0.0.45 diff --git a/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt index b0ec500091..5033b43e22 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt @@ -54,6 +54,7 @@ import com.agileburo.anytype.core_utils.common.EventWrapper import com.agileburo.anytype.core_utils.ext.* import com.agileburo.anytype.core_utils.ext.PopupExtensions.calculateRectInWindow import com.agileburo.anytype.di.common.componentManager +import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.block.model.Block.Content.Text import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.ext.getFirstLinkMarkupParam @@ -505,9 +506,9 @@ open class PageFragment : UiBlock.TOGGLE -> vm.onAddTextBlockClicked(Text.Style.TOGGLE) UiBlock.CODE -> vm.onAddTextBlockClicked(Text.Style.CODE_SNIPPET) UiBlock.PAGE -> vm.onAddNewPageClicked() - UiBlock.FILE -> vm.onAddFileBlockClicked() - UiBlock.IMAGE -> vm.onAddImageBlockClicked() - UiBlock.VIDEO -> vm.onAddVideoBlockClicked() + UiBlock.FILE -> vm.onAddFileBlockClicked(Block.Content.File.Type.FILE) + UiBlock.IMAGE -> vm.onAddFileBlockClicked(Block.Content.File.Type.IMAGE) + UiBlock.VIDEO -> vm.onAddFileBlockClicked(Block.Content.File.Type.VIDEO) UiBlock.BOOKMARK -> vm.onAddBookmarkBlockClicked() UiBlock.LINE_DIVIDER -> vm.onAddDividerBlockClicked() else -> toast(NOT_IMPLEMENTED_MESSAGE) 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 acbaeccde4..b95b763004 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 @@ -1168,18 +1168,29 @@ class PageViewModel( } fun onAddTextBlockClicked(style: Content.Text.Style) { - controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected) - proceedWithCreatingNewTextBlock( - id = orchestrator.stores.focus.current().id, - style = style - ) - } - fun onAddVideoBlockClicked() { - proceedWithCreatingEmptyFileBlock( - id = orchestrator.stores.focus.current().id, - type = Content.File.Type.VIDEO - ) + val target = blocks.first { it.id == orchestrator.stores.focus.current().id } + + val content = target.content + + if (content is Content.Text && content.text.isEmpty()) { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.CRUD.Replace( + context = context, + target = target.id, + prototype = Prototype.Text(style = style) + ) + ) + } + } else { + proceedWithCreatingNewTextBlock( + id = orchestrator.stores.focus.current().id, + style = style + ) + } + + controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected) } private fun onAddLocalVideoClicked(blockId: String) { @@ -1217,18 +1228,21 @@ class PageViewModel( dispatch(Command.OpenGallery(mediaType = MIME_FILE_ALL)) } - fun onAddImageBlockClicked() { - proceedWithCreatingEmptyFileBlock( - id = orchestrator.stores.focus.current().id, - type = Content.File.Type.IMAGE - ) - } + fun onAddFileBlockClicked(type: Content.File.Type) { + val target = blocks.first { it.id == orchestrator.stores.focus.current().id } + val content = target.content - fun onAddFileBlockClicked() { - proceedWithCreatingEmptyFileBlock( - id = orchestrator.stores.focus.current().id, - type = Content.File.Type.FILE - ) + if (content is Content.Text && content.text.isEmpty()) { + proceedWithReplacingByEmptyFileBlock( + id = target.id, + type = type + ) + } else { + proceedWithCreatingEmptyFileBlock( + id = target.id, + type = type + ) + } } private fun proceedWithCreatingEmptyFileBlock( @@ -1249,6 +1263,22 @@ class PageViewModel( } } + private fun proceedWithReplacingByEmptyFileBlock( + id: String, + type: Content.File.Type, + state: Content.File.State = Content.File.State.EMPTY + ) { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.CRUD.Replace( + context = context, + target = id, + prototype = Prototype.File(type = type, state = state) + ) + ) + } + } + fun onCheckboxClicked(view: BlockView.Text.Checkbox) { blocks = blocks.map { block -> @@ -1511,17 +1541,33 @@ class PageViewModel( } fun onAddDividerBlockClicked() { - controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected) - viewModelScope.launch { - orchestrator.proxies.intents.send( - Intent.CRUD.Create( - context = context, - target = orchestrator.stores.focus.current().id, - position = Position.BOTTOM, - prototype = Prototype.Divider + val target = blocks.first { it.id == orchestrator.stores.focus.current().id } + val content = target.content + + if (content is Content.Text && content.text.isEmpty()) { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.CRUD.Replace( + context = context, + target = target.id, + prototype = Prototype.Divider + ) ) - ) + } + } else { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.CRUD.Create( + context = context, + target = target.id, + position = Position.BOTTOM, + prototype = Prototype.Divider + ) + ) + } } + + controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected) } private fun proceedWithUpdatingTextStyle( @@ -1639,17 +1685,32 @@ class PageViewModel( } fun onAddBookmarkBlockClicked() { - controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected) - viewModelScope.launch { - orchestrator.proxies.intents.send( - Intent.CRUD.Create( - context = context, - position = Position.BOTTOM, - target = orchestrator.stores.focus.current().id, - prototype = Prototype.Bookmark + val target = blocks.first { it.id == orchestrator.stores.focus.current().id } + val content = target.content + + if (content is Content.Text && content.text.isEmpty()) { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.CRUD.Replace( + context = context, + target = target.id, + prototype = Prototype.Bookmark + ) ) - ) + } + } else { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.CRUD.Create( + context = context, + position = Position.BOTTOM, + target = target.id, + prototype = Prototype.Bookmark + ) + ) + } } + controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected) } fun onAddBookmarkUrl(target: String, url: String) { diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/page/editor/EditorAddBlockTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/page/editor/EditorAddBlockTest.kt new file mode 100644 index 0000000000..c6f098bf07 --- /dev/null +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/page/editor/EditorAddBlockTest.kt @@ -0,0 +1,189 @@ +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.interactor.ReplaceBlock +import com.agileburo.anytype.domain.block.model.Block +import com.agileburo.anytype.domain.block.model.Position +import com.agileburo.anytype.domain.event.model.Payload +import com.agileburo.anytype.presentation.util.CoroutinesTestRule +import com.nhaarman.mockitokotlin2.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.MockitoAnnotations + +class EditorAddBlockTest : EditorPresentationTestSetup() { + + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun `should replace currently focused text block instead of adding a new block after this text block if this text block is empty`() { + + // SETUP + + val text = "" + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields(emptyMap()), + content = Block.Content.Text( + text = text, + marks = emptyList(), + style = Block.Content.Text.Style.values().random() + ), + children = emptyList() + ) + + val page = listOf( + Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Page( + style = Block.Content.Page.Style.SET + ), + children = listOf(block.id) + ), + block + ) + + val newStyle = Block.Content.Text.Style.values().random() + + val params = ReplaceBlock.Params( + context = root, + target = block.id, + prototype = Block.Prototype.Text( + style = newStyle + ) + ) + + stubInterceptEvents() + stubOpenDocument(document = page) + stubReplaceBlock(params) + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged(id = block.id, hasFocus = true) + onAddBlockToolbarClicked() + } + + // User is now selecting a new text block + + vm.onAddTextBlockClicked(style = newStyle) + + verifyZeroInteractions(createBlock) + + verifyBlocking(replaceBlock, times(1)) { invoke(params) } + } + + @Test + fun `should add new text block after currently focused text block if this focused text block is not empty`() { + + // SETUP + + val text = MockDataFactory.randomString() + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields(emptyMap()), + content = Block.Content.Text( + text = text, + marks = emptyList(), + style = Block.Content.Text.Style.values().random() + ), + children = emptyList() + ) + + val page = listOf( + Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Page( + style = Block.Content.Page.Style.SET + ), + children = listOf(block.id) + ), + block + ) + + val newStyle = Block.Content.Text.Style.values().random() + + val params = CreateBlock.Params( + context = root, + target = block.id, + prototype = Block.Prototype.Text( + style = newStyle + ), + position = Position.BOTTOM + ) + + stubInterceptEvents() + stubOpenDocument(document = page) + stubCreateBlock(params) + + + val vm = buildViewModel() + + // TESTING + + vm.apply { + onStart(root) + onBlockFocusChanged(id = block.id, hasFocus = true) + onAddBlockToolbarClicked() + } + + // User is now selecting a new text block + + vm.onAddTextBlockClicked(style = newStyle) + + verifyZeroInteractions(replaceBlock) + verifyBlocking(createBlock, times(1)) { invoke(params) } + } + + private fun stubReplaceBlock( + params: ReplaceBlock.Params + ) { + replaceBlock.stub { + onBlocking { invoke(params) } doReturn Either.Right( + Pair( + MockDataFactory.randomUuid(), + Payload( + context = root, + emptyList() + ) + ) + ) + } + } + + private fun stubCreateBlock( + params: CreateBlock.Params + ) { + createBlock.stub { + onBlocking { invoke(params) } doReturn Either.Right( + Pair( + MockDataFactory.randomUuid(), + Payload( + context = root, + emptyList() + ) + ) + ) + } + } +} \ No newline at end of file