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

Title as simple block (#949)

This commit is contained in:
Evgenii Kozlov 2020-10-07 11:56:47 +03:00 committed by GitHub
parent 98e9689cd4
commit ce955a7a44
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 2005 additions and 759 deletions

View file

@ -1,6 +1,6 @@
# Change log for Android @Anytype app.
## Version 0.0.50 (WIP)
## Version 0.0.50
### New features 🚀
@ -24,6 +24,10 @@
* Clicking on empty space before document is loaded should not crash application (#930)
* Focusing on start may crash application by some users (#931)
### Middleware ⚙
* Updated middleware protocol to `0.13.18` (#851)
## Version 0.0.49
### Fixes & tech 🚒

View file

@ -2,10 +2,12 @@ package com.anytypeio.anytype.di.feature
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.domain.icon.SetDocumentEmojiIcon
import com.anytypeio.anytype.emojifier.data.Emoji
import com.anytypeio.anytype.emojifier.suggest.EmojiSuggester
import com.anytypeio.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModelFactory
import com.anytypeio.anytype.presentation.util.Bridge
import com.anytypeio.anytype.ui.page.modals.DocumentEmojiIconPickerFragment
import dagger.Module
import dagger.Provides
@ -31,11 +33,13 @@ class DocumentEmojiIconPickerModule {
@PerScreen
fun provideDocumentEmojiIconPickerViewModel(
setEmojiIcon: SetDocumentEmojiIcon,
emojiSuggester: EmojiSuggester
emojiSuggester: EmojiSuggester,
bridge: Bridge<Payload>
): DocumentEmojiIconPickerViewModelFactory = DocumentEmojiIconPickerViewModelFactory(
setEmojiIcon = setEmojiIcon,
emojiSuggester = emojiSuggester,
emojiProvider = Emoji
emojiProvider = Emoji,
bridge = bridge
)
@Provides

View file

@ -2,9 +2,11 @@ package com.anytypeio.anytype.di.feature
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.domain.icon.SetDocumentEmojiIcon
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
import com.anytypeio.anytype.presentation.page.picker.DocumentIconActionMenuViewModelFactory
import com.anytypeio.anytype.presentation.util.Bridge
import com.anytypeio.anytype.ui.page.modals.actions.DocumentIconActionMenuFragment
import com.anytypeio.anytype.ui.page.modals.actions.ProfileIconActionMenuFragment
import dagger.Module
@ -32,10 +34,12 @@ class DocumentIconActionMenuModule {
@PerScreen
fun provideDocumentIconActionMenuViewModelFactory(
setEmojiIcon: SetDocumentEmojiIcon,
setImageIcon: SetDocumentImageIcon
setImageIcon: SetDocumentImageIcon,
bridge: Bridge<Payload>
): DocumentIconActionMenuViewModelFactory = DocumentIconActionMenuViewModelFactory(
setEmojiIcon = setEmojiIcon,
setImageIcon = setImageIcon
setImageIcon = setImageIcon,
bridge = bridge
)
@Provides

View file

@ -13,6 +13,7 @@ import com.anytypeio.anytype.domain.download.DownloadFile
import com.anytypeio.anytype.domain.download.Downloader
import com.anytypeio.anytype.domain.event.interactor.EventChannel
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.domain.icon.DocumentEmojiIconProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.page.*
@ -26,6 +27,7 @@ import com.anytypeio.anytype.presentation.page.editor.Orchestrator
import com.anytypeio.anytype.presentation.page.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.page.selection.SelectionStateHolder
import com.anytypeio.anytype.presentation.page.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.util.Bridge
import com.anytypeio.anytype.ui.page.PageFragment
import dagger.Module
import dagger.Provides
@ -89,7 +91,8 @@ object EditorSessionModule {
archiveDocument: ArchiveDocument,
interactor: Orchestrator,
getListPages: GetListPages,
analytics: Analytics
analytics: Analytics,
bridge: Bridge<Payload>
): PageViewModelFactory = PageViewModelFactory(
openPage = openPage,
closePage = closePage,
@ -105,7 +108,8 @@ object EditorSessionModule {
archiveDocument = archiveDocument,
interactor = interactor,
getListPages = getListPages,
analytics = analytics
analytics = analytics,
bridge = bridge
)
@JvmStatic

View file

@ -0,0 +1,17 @@
package com.anytypeio.anytype.di.main
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.presentation.util.Bridge
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
object BridgeModule {
@JvmStatic
@Provides
@Singleton
fun providePayloadPridge(): Bridge<Payload> = Bridge()
}

View file

@ -16,7 +16,8 @@ import javax.inject.Singleton
UtilModule::class,
EmojiModule::class,
ClipboardModule::class,
AnalyticsModule::class
AnalyticsModule::class,
BridgeModule::class
]
)
interface MainComponent {

View file

@ -51,8 +51,10 @@ class DocumentIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_page
@Inject
lateinit var factory: DocumentIconActionMenuViewModelFactory
@Inject
lateinit var analytics: Analytics
private val vm by viewModels<DocumentIconActionMenuViewModel> { factory }
override fun onActivityCreated(savedInstanceState: Bundle?) {

View file

@ -58,7 +58,7 @@ data class BlockEntity(
}
data class Layout(val type: Type) : Content() {
enum class Type { ROW, COLUMN, DIV }
enum class Type { ROW, COLUMN, DIV, HEADER }
}
data class Icon(

View file

@ -129,11 +129,11 @@ class BlockDataRepository(
override suspend fun setDocumentEmojiIcon(
command: Command.SetDocumentEmojiIcon
) = factory.remote.setDocumentEmojiIcon(command.toEntity())
): Payload = factory.remote.setDocumentEmojiIcon(command.toEntity()).toDomain()
override suspend fun setDocumentImageIcon(
command: Command.SetDocumentImageIcon
) = factory.remote.setDocumentImageIcon(command.toEntity())
): Payload = factory.remote.setDocumentImageIcon(command.toEntity()).toDomain()
override suspend fun setupBookmark(
command: Command.SetupBookmark

View file

@ -32,8 +32,8 @@ interface BlockDataStore {
suspend fun openProfile(id: String): PayloadEntity
suspend fun closePage(id: String)
suspend fun closeDashboard(id: String)
suspend fun setDocumentEmojiIcon(command: CommandEntity.SetDocumentEmojiIcon)
suspend fun setDocumentImageIcon(command: CommandEntity.SetDocumentImageIcon)
suspend fun setDocumentEmojiIcon(command: CommandEntity.SetDocumentEmojiIcon): PayloadEntity
suspend fun setDocumentImageIcon(command: CommandEntity.SetDocumentImageIcon): PayloadEntity
suspend fun setupBookmark(command: CommandEntity.SetupBookmark) : PayloadEntity
suspend fun undo(command: CommandEntity.Undo) : PayloadEntity
suspend fun redo(command: CommandEntity.Redo) : PayloadEntity

View file

@ -31,8 +31,8 @@ interface BlockRemote {
suspend fun closePage(id: String)
suspend fun openDashboard(contextId: String, id: String): PayloadEntity
suspend fun closeDashboard(id: String)
suspend fun setDocumentEmojiIcon(command: CommandEntity.SetDocumentEmojiIcon)
suspend fun setDocumentImageIcon(command: CommandEntity.SetDocumentImageIcon)
suspend fun setDocumentEmojiIcon(command: CommandEntity.SetDocumentEmojiIcon): PayloadEntity
suspend fun setDocumentImageIcon(command: CommandEntity.SetDocumentImageIcon): PayloadEntity
suspend fun uploadBlock(command: CommandEntity.UploadBlock): PayloadEntity
suspend fun setupBookmark(command: CommandEntity.SetupBookmark) : PayloadEntity
suspend fun undo(command: CommandEntity.Undo) : PayloadEntity

View file

@ -91,11 +91,11 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
override suspend fun setDocumentEmojiIcon(
command: CommandEntity.SetDocumentEmojiIcon
) = remote.setDocumentEmojiIcon(command)
): PayloadEntity = remote.setDocumentEmojiIcon(command)
override suspend fun setDocumentImageIcon(
command: CommandEntity.SetDocumentImageIcon
) = remote.setDocumentImageIcon(command)
): PayloadEntity = remote.setDocumentImageIcon(command)
override suspend fun setupBookmark(
command: CommandEntity.SetupBookmark

View file

@ -135,7 +135,7 @@ data class Block(
}
data class Layout(val type: Type) : Content() {
enum class Type { ROW, COLUMN, DIV }
enum class Type { ROW, COLUMN, DIV, HEADER }
}
data class Page(val style: Style) : Content() {

View file

@ -89,8 +89,8 @@ interface BlockRepository {
*/
suspend fun uploadBlock(command: Command.UploadBlock): Payload
suspend fun setDocumentEmojiIcon(command: Command.SetDocumentEmojiIcon)
suspend fun setDocumentImageIcon(command: Command.SetDocumentImageIcon)
suspend fun setDocumentEmojiIcon(command: Command.SetDocumentEmojiIcon): Payload
suspend fun setDocumentImageIcon(command: Command.SetDocumentImageIcon): Payload
suspend fun setupBookmark(command: Command.SetupBookmark): Payload

View file

@ -27,6 +27,29 @@ fun Map<Id, List<Block>>.descendants(parent: Id): List<Id> {
return result
}
/**
* Finds title block for a [Document]
* @return title block
* @throws NoSuchElementException if there was no title block in this document.
*/
fun Document.title(): Block {
val header = first { block ->
val cnt = block.content
cnt is Content.Layout && cnt.type == Content.Layout.Type.HEADER
}
val children = filter { header.children.contains(it.id) }
return children.first { child ->
val cnt = child.content
cnt is Content.Text && cnt.style == Content.Text.Style.TITLE
}
}
fun Document.titleId(): Id? {
return find { block ->
block.content is Content.Text && block.content.isTitle()
}?.id
}
/**
* Transform block structure for rendering purposes.
* @param anchor a root or a parent block for some children blocks.

View file

@ -4,13 +4,14 @@ import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.block.model.Command
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.common.Id
import com.anytypeio.anytype.domain.event.model.Payload
/**
* Use-case for setting emoji icon.
*/
class SetDocumentEmojiIcon(
private val repo: BlockRepository
) : BaseUseCase<Any, SetDocumentEmojiIcon.Params>() {
) : BaseUseCase<Payload, SetDocumentEmojiIcon.Params>() {
override suspend fun run(params: Params) = safe {
repo.setDocumentEmojiIcon(

View file

@ -4,10 +4,11 @@ import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.block.model.Block
import com.anytypeio.anytype.domain.block.model.Command
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.event.model.Payload
class SetDocumentImageIcon(
private val repo: BlockRepository
) : BaseUseCase<Unit, SetDocumentImageIcon.Params>() {
) : BaseUseCase<Payload, SetDocumentImageIcon.Params>() {
override suspend fun run(params: Params) = safe {
val hash = repo.uploadFile(

View file

@ -893,4 +893,127 @@ class BlockExtensionTest {
actual = childrenIdsList
)
}
@Test
fun `should return title block for document`() {
val root = MockDataFactory.randomUuid()
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val a = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, a.id)
)
val document = listOf(page, header, title, a)
val result = document.title()
val expected = title
assertEquals(expected = expected, actual = result)
}
@Test(expected = NoSuchElementException::class)
fun `should throw NoSuchElementException when title block is not present in header childs`() {
val root = MockDataFactory.randomUuid()
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf()
)
val a = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, a.id)
)
val document = listOf(page, header, a)
val result = document.title()
}
@Test(expected = NoSuchElementException::class)
fun `should throw NoSuchElementException when header is not present`() {
val root = MockDataFactory.randomUuid()
val a = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(a.id)
)
val document = listOf(page, a)
val result = document.title()
}
}

View file

@ -109,11 +109,11 @@ class BlockMiddleware(
override suspend fun setDocumentEmojiIcon(
command: CommandEntity.SetDocumentEmojiIcon
) = middleware.setDocumentEmojiIcon(command)
): PayloadEntity = middleware.setDocumentEmojiIcon(command)
override suspend fun setDocumentImageIcon(
command: CommandEntity.SetDocumentImageIcon
) = middleware.setDocumentImageIcon(command)
): PayloadEntity = middleware.setDocumentImageIcon(command)
override suspend fun setupBookmark(
command: CommandEntity.SetupBookmark

View file

@ -101,6 +101,7 @@ fun Block.layout(): BlockEntity.Content.Layout = BlockEntity.Content.Layout(
Block.Content.Layout.Style.Column -> BlockEntity.Content.Layout.Type.COLUMN
Block.Content.Layout.Style.Row -> BlockEntity.Content.Layout.Type.ROW
Block.Content.Layout.Style.Div -> BlockEntity.Content.Layout.Type.DIV
Block.Content.Layout.Style.Header -> BlockEntity.Content.Layout.Type.HEADER
else -> throw IllegalStateException("Unexpected layout style: ${layout.style}")
}
)

View file

@ -765,7 +765,7 @@ public class Middleware {
return new Pair<>(response.getBlockId(), mapper.toPayload(response.getEvent()));
}
public void setDocumentEmojiIcon(CommandEntity.SetDocumentEmojiIcon command) throws Exception {
public PayloadEntity setDocumentEmojiIcon(CommandEntity.SetDocumentEmojiIcon command) throws Exception {
Value emojiValue = Value.newBuilder().setStringValue(command.getEmoji()).build();
Value imageValue = Value.newBuilder().setStringValue("").build();
@ -798,9 +798,11 @@ public class Middleware {
if (BuildConfig.DEBUG) {
Timber.d(response.getClass().getName() + "\n" + response.toString());
}
return mapper.toPayload(response.getEvent());
}
public void setDocumentImageIcon(CommandEntity.SetDocumentImageIcon command) throws Exception {
public PayloadEntity setDocumentImageIcon(CommandEntity.SetDocumentImageIcon command) throws Exception {
Value imageValue = Value.newBuilder().setStringValue(command.getHash()).build();
Value emojiValue = Value.newBuilder().setStringValue("").build();
@ -833,6 +835,8 @@ public class Middleware {
if (BuildConfig.DEBUG) {
Timber.d(response.getClass().getName() + "\n" + response.toString());
}
return mapper.toPayload(response.getEvent());
}
public PayloadEntity setupBookmark(CommandEntity.SetupBookmark command) throws Exception {

View file

@ -74,6 +74,7 @@ import com.anytypeio.anytype.presentation.page.render.BlockViewRenderer
import com.anytypeio.anytype.presentation.page.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.page.selection.SelectionStateHolder
import com.anytypeio.anytype.presentation.page.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.util.Bridge
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@ -96,7 +97,8 @@ class PageViewModel(
private val renderer: DefaultBlockViewRenderer,
private val orchestrator: Orchestrator,
private val getListPages: GetListPages,
private val analytics: Analytics
private val analytics: Analytics,
private val bridge: Bridge<Payload>
) : ViewStateViewModel<ViewState>(),
SupportNavigation<EventWrapper<AppNavigation.Command>>,
SupportCommand<Command>,
@ -110,7 +112,7 @@ class PageViewModel(
private val views: List<BlockView> get() = orchestrator.stores.views.current()
private var eventSubscription: Job? = null
private val jobs = mutableListOf<Job>()
private var mode = EditorMode.EDITING
@ -192,7 +194,6 @@ class PageViewModel(
}
private fun proceedWithUpdatingDocumentTitle(update: String) {
viewModelScope.launch {
orchestrator.proxies.intents.send(
Intent.Document.UpdateTitle(
@ -471,13 +472,20 @@ class PageViewModel(
stateData.postValue(ViewState.Loading)
eventSubscription = viewModelScope.launch {
jobs += viewModelScope.launch {
interceptEvents
.build(InterceptEvents.Params(context))
.map { events -> processEvents(events) }
.collect { refresh() }
}
jobs += viewModelScope.launch {
bridge
.flow()
.filter { it.context == context }
.collect { orchestrator.proxies.payloads.send(it) }
}
viewModelScope.launch {
openPage(OpenPage.Params(id)).proceed(
success = { result ->
@ -506,16 +514,23 @@ class PageViewModel(
val event = payload.events.find { it is Event.Command.ShowBlock }
if (event is Event.Command.ShowBlock) {
val root = event.blocks.find { it.id == context }
if (root == null) {
Timber.e("Could not find the root block on initial focusing")
} else if (root.children.isEmpty()) {
val focus = Editor.Focus(id = root.id, cursor = Editor.Cursor.End)
viewModelScope.launch { orchestrator.stores.focus.update(focus) }
controlPanelInteractor.onEvent(
ControlPanelMachine.Event.OnFocusChanged(
id = root.id, style = Content.Text.Style.TITLE
)
)
when {
root == null -> Timber.e("Could not find the root block on initial focusing")
root.children.size == 1 -> {
val first = event.blocks.first { it.id == root.children.first() }
val content = first.content
if (content is Content.Layout && content.type == Content.Layout.Type.HEADER) {
val title = event.blocks.title()
val focus = Editor.Focus(id = title.id, cursor = Editor.Cursor.End)
viewModelScope.launch { orchestrator.stores.focus.update(focus) }
controlPanelInteractor.onEvent(
ControlPanelMachine.Event.OnFocusChanged(
id = title.id, style = Content.Text.Style.TITLE
)
)
}
}
else -> Timber.d("Skipping initial focusing, document is not empty.")
}
}
}
@ -859,6 +874,7 @@ class PageViewModel(
target = id
)
} else {
if (previous is BlockView.Title) _toasts.offer("Merging with title currently not supported")
Timber.d("Skipping merge because previous block is not a text block")
}
} else {
@ -1301,26 +1317,10 @@ 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 = id,
id = target.id,
style = style,
position = position
position = Position.BOTTOM
)
}
@ -1403,37 +1403,18 @@ class PageViewModel(
}
fun onAddFileBlockClicked(type: Content.File.Type) {
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 = 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 = focused.id,
type = type,
position = position
position = Position.BOTTOM
)
}
}
@ -1511,8 +1492,10 @@ class PageViewModel(
}
fun onBlockToolbarStyleClicked() {
if (orchestrator.stores.focus.current().id == context) {
_toasts.offer("Changing style for title currently not supported")
val target = orchestrator.stores.focus.current().id
val view = views.first { it.id == target }
if (view is BlockView.Title) {
_toasts.offer(CANNOT_OPEN_STYLE_PANEL_FOR_TITLE_ERROR)
} else {
val textSelection = orchestrator.stores.textSelection.current()
controlPanelInteractor.onEvent(
@ -1534,14 +1517,12 @@ class PageViewModel(
}
fun onBlockToolbarBlockActionsClicked() {
if (orchestrator.stores.focus.current().id == context) {
_toasts.offer("Not implemented for title")
val target = orchestrator.stores.focus.current().id
val view = views.first { it.id == target }
if (view is BlockView.Title) {
_toasts.offer(CANNOT_OPEN_ACTION_MENU_FOR_TITLE_ERROR)
} else {
dispatch(
Command.Measure(
target = orchestrator.stores.focus.current().id
)
)
dispatch(Command.Measure(target = target))
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.BTN_BLOCK_ACTIONS
@ -1708,7 +1689,7 @@ class PageViewModel(
private fun onSelectAllClicked(state: ViewState.Success) =
state.blocks.map { block ->
if (block.id != context) select(block.id)
if (block.id != blocks.titleId()) select(block.id)
block.updateSelection(newSelection = true)
}.let {
onMultiSelectModeBlockClicked()
@ -1717,7 +1698,7 @@ class PageViewModel(
private fun onUnselectAllClicked(state: ViewState.Success) =
state.blocks.map { block ->
if (block.id != context) unselect(block.id)
unselect(block.id)
block.updateSelection(newSelection = false)
}.let {
onMultiSelectModeBlockClicked()
@ -1892,6 +1873,7 @@ class PageViewModel(
}
fun onOutsideClicked() {
val root = blocks.find { it.id == context } ?: return
if (root.children.isEmpty()) {
@ -1928,6 +1910,9 @@ class PageViewModel(
is Content.Divider -> {
addNewBlockAtTheEnd()
}
is Content.Layout -> {
addNewBlockAtTheEnd()
}
else -> {
Timber.d("Outside-click has been ignored.")
}
@ -2740,6 +2725,11 @@ class PageViewModel(
const val CANNOT_MOVE_PARENT_INTO_CHILD =
"Cannot move parent into child. Please, check selected blocks."
const val MENTION_TITLE_EMPTY = "Untitled"
const val CANNOT_OPEN_ACTION_MENU_FOR_TITLE_ERROR =
"Opening action menu for title currently not supported"
const val CANNOT_OPEN_STYLE_PANEL_FOR_TITLE_ERROR =
"Opening style panel for title currently not supported"
}
data class MarkupAction(
@ -2767,7 +2757,10 @@ class PageViewModel(
fun onStop() {
Timber.d("onStop")
eventSubscription?.cancel()
jobs.apply {
forEach { it.cancel() }
clear()
}
}
enum class Session { IDLE, OPEN, ERROR }

View file

@ -8,12 +8,14 @@ import com.anytypeio.anytype.domain.block.interactor.UpdateLinkMarks
import com.anytypeio.anytype.domain.block.model.Block
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.event.model.Event
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.page.*
import com.anytypeio.anytype.domain.page.navigation.GetListPages
import com.anytypeio.anytype.presentation.common.StateReducer
import com.anytypeio.anytype.presentation.page.editor.Orchestrator
import com.anytypeio.anytype.presentation.page.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.util.Bridge
open class PageViewModelFactory(
private val openPage: OpenPage,
@ -30,7 +32,8 @@ open class PageViewModelFactory(
private val renderer: DefaultBlockViewRenderer,
private val interactor: Orchestrator,
private val getListPages: GetListPages,
private val analytics: Analytics
private val analytics: Analytics,
private val bridge: Bridge<Payload>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -50,7 +53,8 @@ open class PageViewModelFactory(
createNewDocument = createNewDocument,
orchestrator = interactor,
getListPages = getListPages,
analytics = analytics
analytics = analytics,
bridge = bridge
) as T
}
}

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.presentation.page.picker
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.domain.common.Id
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.domain.icon.SetDocumentEmojiIcon
import com.anytypeio.anytype.emojifier.data.Emoji
import com.anytypeio.anytype.emojifier.data.EmojiProvider
@ -10,6 +11,7 @@ import com.anytypeio.anytype.emojifier.suggest.EmojiSuggester
import com.anytypeio.anytype.emojifier.suggest.model.EmojiSuggest
import com.anytypeio.anytype.library_page_icon_picker_widget.model.EmojiPickerView
import com.anytypeio.anytype.presentation.page.editor.Proxy
import com.anytypeio.anytype.presentation.util.Bridge
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -19,7 +21,8 @@ import timber.log.Timber
class DocumentEmojiIconPickerViewModel(
private val setEmojiIcon: SetDocumentEmojiIcon,
private val provider: EmojiProvider,
private val suggester: EmojiSuggester
private val suggester: EmojiSuggester,
private val bridge: Bridge<Payload>
) : ViewModel() {
/**
@ -126,7 +129,10 @@ class DocumentEmojiIconPickerViewModel(
)
).proceed(
failure = { Timber.e(it, "Error while setting emoji") },
success = { state.apply { value = ViewState.Exit } }
success = {
bridge.send(it)
state.apply { value = ViewState.Exit }
}
)
}
}

View file

@ -2,14 +2,17 @@ package com.anytypeio.anytype.presentation.page.picker
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.domain.icon.SetDocumentEmojiIcon
import com.anytypeio.anytype.emojifier.data.EmojiProvider
import com.anytypeio.anytype.emojifier.suggest.EmojiSuggester
import com.anytypeio.anytype.presentation.util.Bridge
class DocumentEmojiIconPickerViewModelFactory(
private val setEmojiIcon: SetDocumentEmojiIcon,
private val emojiSuggester: EmojiSuggester,
private val emojiProvider: EmojiProvider
private val emojiProvider: EmojiProvider,
private val bridge: Bridge<Payload>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -17,7 +20,8 @@ class DocumentEmojiIconPickerViewModelFactory(
return DocumentEmojiIconPickerViewModel(
setEmojiIcon = setEmojiIcon,
suggester = emojiSuggester,
provider = emojiProvider
provider = emojiProvider,
bridge = bridge
) as T
}
}

View file

@ -2,12 +2,14 @@ package com.anytypeio.anytype.presentation.page.picker
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_utils.ui.ViewStateViewModel
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.domain.icon.SetDocumentEmojiIcon
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
import com.anytypeio.anytype.emojifier.data.Emoji
import com.anytypeio.anytype.presentation.common.StateReducer
import com.anytypeio.anytype.presentation.page.picker.DocumentIconActionMenuViewModel.Contract.*
import com.anytypeio.anytype.presentation.page.picker.DocumentIconActionMenuViewModel.ViewState
import com.anytypeio.anytype.presentation.util.Bridge
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.*
@ -15,7 +17,8 @@ import kotlinx.coroutines.launch
class DocumentIconActionMenuViewModel(
private val setEmojiIcon: SetDocumentEmojiIcon,
private val setImageIcon: SetDocumentImageIcon
private val setImageIcon: SetDocumentImageIcon,
private val bridge: Bridge<Payload>
) : ViewStateViewModel<ViewState>(), StateReducer<State, Event> {
private val events = ConflatedBroadcastChannel<Event>()
@ -49,7 +52,10 @@ class DocumentIconActionMenuViewModel(
context = action.context
)
).proceed(
success = { events.send(Event.OnCompleted) },
success = {
bridge.send(it)
events.send(Event.OnCompleted)
},
failure = { events.send(Event.Failure(it)) }
)
is Action.ClearEmoji -> setEmojiIcon(
@ -59,7 +65,10 @@ class DocumentIconActionMenuViewModel(
context = action.context
)
).proceed(
success = { events.send(Event.OnCompleted) },
success = {
bridge.send(it)
events.send(Event.OnCompleted)
},
failure = { events.send(Event.Failure(it)) }
)
is Action.SetImageIcon -> setImageIcon(
@ -69,7 +78,10 @@ class DocumentIconActionMenuViewModel(
)
).proceed(
failure = { events.send(Event.Failure(it)) },
success = { events.send(Event.OnCompleted) }
success = {
bridge.send(it)
events.send(Event.OnCompleted)
}
)
is Action.PickRandomEmoji -> {
val random = Emoji.DATA.random().random()

View file

@ -2,17 +2,21 @@ package com.anytypeio.anytype.presentation.page.picker
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.domain.event.model.Payload
import com.anytypeio.anytype.domain.icon.SetDocumentEmojiIcon
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
import com.anytypeio.anytype.presentation.util.Bridge
class DocumentIconActionMenuViewModelFactory(
private val setEmojiIcon: SetDocumentEmojiIcon,
private val setImageIcon: SetDocumentImageIcon
private val setImageIcon: SetDocumentImageIcon,
private val bridge: Bridge<Payload>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = DocumentIconActionMenuViewModel(
setEmojiIcon = setEmojiIcon,
setImageIcon = setImageIcon
setImageIcon = setImageIcon,
bridge = bridge
) as T
}

View file

@ -11,6 +11,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.page.EditorMode
import com.anytypeio.anytype.presentation.mapper.*
import com.anytypeio.anytype.presentation.page.toggle.ToggleStateHolder
import timber.log.Timber
class DefaultBlockViewRenderer(
private val counter: Counter,
@ -31,14 +32,19 @@ class DefaultBlockViewRenderer(
val result = mutableListOf<BlockView>()
buildTitle(
mode = mode,
anchor = anchor,
root = root,
result = result,
details = details,
focus = focus
)
if (anchor == root.id) {
root.content.let { cnt ->
if (cnt is Content.Smart && cnt.type == Content.Smart.Type.ARCHIVE) {
result.add(
BlockView.Title.Archive(
mode = BlockView.Mode.READ,
id = anchor,
text = details.details[root.id]?.name
)
)
}
}
}
counter.reset()
@ -48,7 +54,16 @@ class DefaultBlockViewRenderer(
when (content.style) {
Content.Text.Style.TITLE -> {
counter.reset()
result.add(title(mode, block, content, root, focus))
result.add(
title(
mode = mode,
block = block,
content = content,
focus = focus,
root = root,
details = details
)
)
}
Content.Text.Style.P -> {
counter.reset()
@ -330,83 +345,6 @@ class DefaultBlockViewRenderer(
return result
}
private fun buildTitle(
mode: EditorMode,
anchor: Id,
root: Block,
result: MutableList<BlockView>,
details: Block.Details,
focus: Focus
) {
if (anchor == root.id) {
val cursor = if (anchor == focus.id) {
focus.cursor?.let { cursor ->
when (cursor) {
is Cursor.Start -> 0
is Cursor.End -> details.details[root.id]?.name?.length ?: 0
is Cursor.Range -> cursor.range.first
}
}
} else {
null
}
val viewMode =
if (mode == EditorMode.EDITING)
BlockView.Mode.EDIT
else
BlockView.Mode.READ
val text = details.details[root.id]?.name
val isFocused = anchor == focus.id
val type = (root.content as? Content.Smart)?.type
result.add(
when (type) {
Content.Smart.Type.PROFILE -> {
BlockView.Title.Profile(
mode = viewMode,
id = anchor,
isFocused = isFocused,
text = text,
image = details.details[root.id]?.iconImage?.let { name ->
if (name.isNotEmpty()) urlBuilder.thumbnail(name) else null
},
cursor = cursor
)
}
Content.Smart.Type.ARCHIVE -> {
BlockView.Title.Archive(
mode = viewMode,
id = anchor,
text = text
)
}
else -> {
BlockView.Title.Document(
mode = viewMode,
id = anchor,
isFocused = isFocused,
text = text,
emoji = details.details[root.id]?.iconEmoji?.let { name ->
if (name.isNotEmpty())
name
else
null
},
image = details.details[root.id]?.iconImage?.let { name ->
if (name.isNotEmpty())
urlBuilder.thumbnail(name)
else
null
},
cursor = cursor
)
}
}
)
}
}
private fun paragraph(
mode: EditorMode,
block: Block,
@ -673,19 +611,66 @@ class DefaultBlockViewRenderer(
block: Block,
content: Content.Text,
root: Block,
focus: Focus
): BlockView.Title.Document = BlockView.Title.Document(
mode = if (mode == EditorMode.EDITING) BlockView.Mode.EDIT else BlockView.Mode.READ,
id = block.id,
text = content.text,
emoji = root.fields.iconEmoji?.let { name ->
if (name.isNotEmpty())
name
else
null
},
isFocused = block.id == focus.id
)
focus: Focus,
details: Block.Details
): BlockView.Title {
Timber.d("Focus while building title: $focus")
val cursor: Int?
cursor = if (focus.id == block.id) {
focus.cursor?.let { crs ->
when (crs) {
is Cursor.Start -> 0
is Cursor.End -> content.text.length
is Cursor.Range -> crs.range.first
}
}
} else {
null
}
val rootContent = root.content
check(rootContent is Content.Smart)
return when (rootContent.type) {
Content.Smart.Type.PAGE -> BlockView.Title.Document(
mode = if (mode == EditorMode.EDITING) BlockView.Mode.EDIT else BlockView.Mode.READ,
id = block.id,
text = content.text,
emoji = details.details[root.id]?.iconEmoji?.let { name ->
if (name.isNotEmpty())
name
else
null
},
image = details.details[root.id]?.iconImage?.let { name ->
if (name.isNotEmpty())
urlBuilder.thumbnail(name)
else
null
},
isFocused = block.id == focus.id,
cursor = cursor
)
Content.Smart.Type.PROFILE -> BlockView.Title.Profile(
mode = if (mode == EditorMode.EDITING) BlockView.Mode.EDIT else BlockView.Mode.READ,
id = block.id,
text = content.text,
image = details.details[root.id]?.iconImage?.let { name ->
if (name.isNotEmpty())
urlBuilder.thumbnail(name)
else
null
},
isFocused = block.id == focus.id,
cursor = cursor
)
else -> throw IllegalStateException("Unexpected root block content: ${root.content}")
}
}
private fun toPages(
block: Block,

View file

@ -0,0 +1,14 @@
package com.anytypeio.anytype.presentation.util
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
/**
* Event bus for passing data between view models.
*/
class Bridge<T> {
private val channel = BroadcastChannel<T>(1)
suspend fun send(t: T) = channel.send(t)
fun flow(): Flow<T> = channel.asFlow()
}

View file

@ -2,16 +2,9 @@ package com.anytypeio.anytype.presentation
import MockDataFactory
import com.anytypeio.anytype.domain.block.model.Block
import java.util.concurrent.ThreadLocalRandom
object MockBlockFactory {
fun randomStyle() : Block.Content.Text.Style {
val styles = Block.Content.Text.Style.values()
val random = ThreadLocalRandom.current().nextInt(styles.size)
return styles[random]
}
fun makeOnePageWithOneTextBlock(
root: String,
child: String,
@ -20,8 +13,8 @@ object MockBlockFactory {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(child)
),

View file

@ -9,6 +9,7 @@ import com.anytypeio.anytype.domain.block.model.Block
import com.anytypeio.anytype.domain.event.model.Event
import com.anytypeio.anytype.domain.ext.content
import com.anytypeio.anytype.presentation.page.editor.ViewState
import com.anytypeio.anytype.presentation.util.TXT
import com.jraska.livedata.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@ -45,12 +46,12 @@ class BlockReadModeTest : PageViewModelTest() {
Block(
id = root,
fields = Block.Fields.empty(),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = blocks.map { it.id }
children = listOf(header.id) + blocks.map { it.id }
)
) + blocks
) + listOf(header, title) + blocks
private val blockViewsReadMode = listOf<BlockView>(
blocks[0].let { p ->
@ -90,6 +91,20 @@ class BlockReadModeTest : PageViewModelTest() {
}
)
private val titleEditModeView = BlockView.Title.Document(
id = title.id,
text = title.content<TXT>().text,
isFocused = false,
mode = BlockView.Mode.EDIT
)
private val titleReadModeView = BlockView.Title.Document(
id = title.id,
text = title.content<TXT>().text,
isFocused = false,
mode = BlockView.Mode.READ
)
private val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
@ -126,18 +141,11 @@ class BlockReadModeTest : PageViewModelTest() {
val testObserver = vm.state.test()
val title = BlockView.Title.Document(
id = root,
text = null,
isFocused = false,
mode = BlockView.Mode.READ
)
val initial = blockViewsReadMode
testObserver.assertValue(
ViewState.Success(
blocks = listOf(title) + initial
blocks = listOf(titleReadModeView) + initial
)
)
}
@ -167,18 +175,11 @@ class BlockReadModeTest : PageViewModelTest() {
val testObserver = vm.state.test()
val title = BlockView.Title.Document(
id = root,
text = null,
isFocused = false,
mode = BlockView.Mode.EDIT
)
val initial = blockViewsEditMode
testObserver.assertValue(
ViewState.Success(
blocks = listOf(title) + initial
blocks = listOf(titleEditModeView) + initial
)
)
}
@ -208,18 +209,11 @@ class BlockReadModeTest : PageViewModelTest() {
val testObserver = vm.state.test()
val title = BlockView.Title.Document(
id = root,
text = null,
isFocused = false,
mode = BlockView.Mode.EDIT
)
val initial = blockViewsEditMode
testObserver.assertValue(
ViewState.Success(
blocks = listOf(title) + initial
blocks = listOf(titleEditModeView) + initial
)
)
}
@ -249,18 +243,11 @@ class BlockReadModeTest : PageViewModelTest() {
val testObserver = vm.state.test()
val title = BlockView.Title.Document(
id = root,
text = null,
isFocused = false,
mode = BlockView.Mode.EDIT
)
val initial = blockViewsEditMode
testObserver.assertValue(
ViewState.Success(
blocks = listOf(title) + initial
blocks = listOf(titleEditModeView) + initial
)
)
}
@ -290,18 +277,11 @@ class BlockReadModeTest : PageViewModelTest() {
val testObserver = vm.state.test()
val title = BlockView.Title.Document(
id = root,
text = null,
isFocused = false,
mode = BlockView.Mode.EDIT
)
val initial = blockViewsEditMode
testObserver.assertValue(
ViewState.Success(
blocks = listOf(title) + initial
blocks = listOf(titleEditModeView) + initial
)
)
}
@ -331,18 +311,11 @@ class BlockReadModeTest : PageViewModelTest() {
val testObserver = vm.state.test()
val title = BlockView.Title.Document(
id = root,
text = null,
isFocused = false,
mode = BlockView.Mode.EDIT
)
val initial = blockViewsEditMode
testObserver.assertValue(
ViewState.Success(
blocks = listOf(title) + initial
blocks = listOf(titleEditModeView) + initial
)
)
}
@ -372,13 +345,6 @@ class BlockReadModeTest : PageViewModelTest() {
val testObserver = vm.state.test()
val title = BlockView.Title.Document(
id = root,
text = null,
isFocused = false,
mode = BlockView.Mode.EDIT
)
val initial = blockViewsEditMode
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
@ -386,7 +352,7 @@ class BlockReadModeTest : PageViewModelTest() {
runBlockingTest {
testObserver.assertValue(
ViewState.Success(
blocks = listOf(title) + initial
blocks = listOf(titleEditModeView) + initial
)
)
}

View file

@ -67,6 +67,26 @@ class DefaultBlockViewRendererTest {
@Test
fun `should return title, paragraph, toggle with its indented inner checkbox`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val paragraph = Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
@ -103,14 +123,14 @@ class DefaultBlockViewRendererTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(paragraph.id, toggle.id),
children = listOf(header.id, paragraph.id, toggle.id),
fields = Block.Fields.empty(),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
)
)
val blocks = listOf(page, paragraph, toggle, checkbox)
val blocks = listOf(page, header, title, paragraph, toggle, checkbox)
val map = blocks.asMap()
@ -135,9 +155,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = null,
text = title.content<Block.Content.Text>().text,
emoji = null
),
BlockView.Text.Paragraph(
@ -168,6 +188,26 @@ class DefaultBlockViewRendererTest {
@Test
fun `should return title, paragraph, toggle without its inner checkbox`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val paragraph = Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
@ -204,14 +244,14 @@ class DefaultBlockViewRendererTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(paragraph.id, toggle.id),
children = listOf(header.id, paragraph.id, toggle.id),
fields = Block.Fields.empty(),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
)
)
val blocks = listOf(page, paragraph, toggle, checkbox)
val blocks = listOf(page, header, title, paragraph, toggle, checkbox)
val map = blocks.asMap()
@ -236,9 +276,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = null,
text = title.content<Block.Content.Text>().text,
emoji = null
),
BlockView.Text.Paragraph(
@ -277,6 +317,27 @@ class DefaultBlockViewRendererTest {
@Test
fun `should return paragraph with null alignment`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val paragraph = Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
@ -291,14 +352,14 @@ class DefaultBlockViewRendererTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(paragraph.id),
children = listOf(header.id, paragraph.id),
fields = Block.Fields.empty(),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
)
)
val blocks = listOf(page, paragraph)
val blocks = listOf(page, header, title, paragraph)
val map = blocks.asMap()
@ -319,9 +380,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = null,
text = title.content<Block.Content.Text>().text,
emoji = null
),
BlockView.Text.Paragraph(
@ -340,6 +401,27 @@ class DefaultBlockViewRendererTest {
@Test
fun `should return paragraph with proper alignment`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val paragraph = Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
@ -354,14 +436,14 @@ class DefaultBlockViewRendererTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(paragraph.id),
children = listOf(header.id, paragraph.id),
fields = Block.Fields.empty(),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
)
)
val blocks = listOf(page, paragraph)
val blocks = listOf(page, header, title, paragraph)
val map = blocks.asMap()
@ -382,9 +464,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = null,
text = title.content<Block.Content.Text>().text,
emoji = null
),
BlockView.Text.Paragraph(
@ -403,6 +485,27 @@ class DefaultBlockViewRendererTest {
@Test
fun `should add profile title when smart block is profile`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val paragraph = Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
@ -425,16 +528,17 @@ class DefaultBlockViewRendererTest {
)
)
val details = mapOf(pageId to fields)
val page = Block(
id = pageId,
children = listOf(paragraph.id),
children = listOf(header.id, paragraph.id),
fields = fields,
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PROFILE
)
)
val blocks = listOf(page, paragraph)
val blocks = listOf(page, header, title, paragraph)
val map = blocks.asMap()
@ -455,9 +559,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Profile(
id = page.id,
id = title.id,
isFocused = false,
text = name,
text = title.content<Block.Content.Text>().text,
image = UrlBuilder(gateway).thumbnail(imageName)
),
BlockView.Text.Paragraph(
@ -476,6 +580,27 @@ class DefaultBlockViewRendererTest {
@Test
fun `should add title when smart block is page`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val paragraph = Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
@ -498,16 +623,17 @@ class DefaultBlockViewRendererTest {
)
)
val details = mapOf(pageId to fields)
val page = Block(
id = pageId,
children = listOf(paragraph.id),
children = listOf(header.id, paragraph.id),
fields = fields,
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
)
)
val blocks = listOf(page, paragraph)
val blocks = listOf(page, header, title, paragraph)
val map = blocks.asMap()
@ -528,9 +654,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = name,
text = title.content<Block.Content.Text>().text,
image = UrlBuilder(gateway).thumbnail(imageName)
),
BlockView.Text.Paragraph(
@ -547,8 +673,28 @@ class DefaultBlockViewRendererTest {
assertEquals(expected = expected, actual = result)
}
@Test
fun `should add title when page is not smart block`() {
@Test(expected = IllegalStateException::class)
fun `should throw exception when smart block type is unexpected`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val paragraph = Block(
id = MockDataFactory.randomUuid(),
@ -565,21 +711,24 @@ class DefaultBlockViewRendererTest {
val name = MockDataFactory.randomString()
val imageName = MockDataFactory.randomString()
val pageId = MockDataFactory.randomUuid()
val fields = Block.Fields(
map = mapOf(
"name" to name,
"iconImage" to imageName
)
)
val details = mapOf(pageId to fields)
val page = Block(
id = pageId,
children = listOf(paragraph.id),
children = listOf(header.id, paragraph.id),
fields = fields,
content = Block.Content.Page(style = Block.Content.Page.Style.TASK)
)
val blocks = listOf(page, paragraph)
val blocks = listOf(page, header, title, paragraph)
val map = blocks.asMap()
@ -588,7 +737,7 @@ class DefaultBlockViewRendererTest {
renderer = renderer
)
val result = runBlocking {
runBlocking {
wrapper.render(
root = page,
anchor = page.id,
@ -597,31 +746,31 @@ class DefaultBlockViewRendererTest {
details = Block.Details(details)
)
}
val expected = listOf(
BlockView.Title.Document(
id = page.id,
isFocused = false,
text = name,
image = UrlBuilder(gateway).thumbnail(imageName)
),
BlockView.Text.Paragraph(
isFocused = true,
id = paragraph.id,
marks = emptyList(),
backgroundColor = paragraph.content<Block.Content.Text>().backgroundColor,
color = paragraph.content<Block.Content.Text>().color,
text = paragraph.content<Block.Content.Text>().text,
alignment = Alignment.CENTER
)
)
assertEquals(expected = expected, actual = result)
}
@Test
fun `should render nested paragraphs`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val c = Block(
id = "C",
children = listOf(),
@ -662,14 +811,14 @@ class DefaultBlockViewRendererTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(a.id),
children = listOf(header.id, a.id),
fields = fields,
content = Block.Content.Smart(type = Block.Content.Smart.Type.PAGE)
)
val details = mapOf(page.id to fields)
val blocks = listOf(page, a, b, c)
val blocks = listOf(page, header, title, a, b, c)
val map = blocks.asMap()
@ -690,9 +839,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = null,
text = title.content<Block.Content.Text>().text,
image = null
),
BlockView.Text.Paragraph(
@ -733,6 +882,26 @@ class DefaultBlockViewRendererTest {
@Test
fun `should render nested checkboxes`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val c = Block(
id = "C",
children = listOf(),
@ -773,14 +942,14 @@ class DefaultBlockViewRendererTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(a.id),
children = listOf(header.id, a.id),
fields = fields,
content = Block.Content.Smart(type = Block.Content.Smart.Type.PAGE)
)
val details = mapOf(page.id to fields)
val blocks = listOf(page, a, b, c)
val blocks = listOf(page, header, title, a, b, c)
val map = blocks.asMap()
@ -801,9 +970,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = null,
text = title.content<Block.Content.Text>().text,
image = null
),
BlockView.Text.Checkbox(
@ -841,6 +1010,26 @@ class DefaultBlockViewRendererTest {
@Test
fun `should render nested bulleted items`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val c = Block(
id = "C",
children = listOf(),
@ -881,14 +1070,14 @@ class DefaultBlockViewRendererTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(a.id),
children = listOf(header.id, a.id),
fields = fields,
content = Block.Content.Smart(type = Block.Content.Smart.Type.PAGE)
)
val details = mapOf(page.id to fields)
val blocks = listOf(page, a, b, c)
val blocks = listOf(page, header, title, a, b, c)
val map = blocks.asMap()
@ -909,9 +1098,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = null,
text = title.content<Block.Content.Text>().text,
image = null
),
BlockView.Text.Bulleted(
@ -949,6 +1138,26 @@ class DefaultBlockViewRendererTest {
@Test
fun `should render nested numbered lists`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val d = Block(
id = "D",
children = listOf(),
@ -1001,14 +1210,14 @@ class DefaultBlockViewRendererTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(a.id),
children = listOf(header.id, a.id),
fields = fields,
content = Block.Content.Smart(type = Block.Content.Smart.Type.PAGE)
)
val details = mapOf(page.id to fields)
val blocks = listOf(page, a, b, c, d)
val blocks = listOf(page, header, title, a, b, c, d)
val map = blocks.asMap()
@ -1029,9 +1238,9 @@ class DefaultBlockViewRendererTest {
val expected = listOf(
BlockView.Title.Document(
id = page.id,
id = title.id,
isFocused = false,
text = null,
text = title.content<Block.Content.Text>().text,
image = null
),
BlockView.Text.Numbered(

View file

@ -50,8 +50,8 @@ class EditorAddBlockTest : EditorPresentationTestSetup() {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(block.id)
),
@ -113,8 +113,8 @@ class EditorAddBlockTest : EditorPresentationTestSetup() {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(block.id)
),

View file

@ -33,6 +33,26 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val child = Block(
id = "CHILD",
fields = Block.Fields.empty(),
@ -61,10 +81,10 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(parent.id)
children = listOf(header.id, parent.id)
)
val document = listOf(page, parent, child)
val document = listOf(page, header, title, parent, child)
val params = UnlinkBlocks.Params(
context = root,
@ -109,9 +129,9 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
ViewState.Success(
listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null
text = title.content<Block.Content.Text>().text
),
BlockView.Text.Paragraph(
id = parent.id,
@ -129,6 +149,26 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val child1 = Block(
id = "CHILD1",
fields = Block.Fields.empty(),
@ -168,10 +208,10 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(parent.id)
children = listOf(header.id, parent.id)
)
val document = listOf(page, parent, child1, child2)
val document = listOf(page, header, title, parent, child1, child2)
val params = UnlinkBlocks.Params(
context = root,
@ -216,9 +256,9 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
ViewState.Success(
listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null
text = title.content<Block.Content.Text>().text
),
BlockView.Text.Paragraph(
id = parent.id,
@ -242,6 +282,26 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val child1 = Block(
id = "CHILD1-TEXT",
fields = Block.Fields.empty(),
@ -292,10 +352,10 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(parent.id)
children = listOf(header.id, parent.id)
)
val document = listOf(page, parent, child1, child2, child3)
val document = listOf(page, header, title, parent, child1, child2, child3)
val params = UnlinkBlocks.Params(
context = root,
@ -340,9 +400,9 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
ViewState.Success(
listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null
text = title.content<Block.Content.Text>().text
),
BlockView.Text.Paragraph(
id = parent.id,
@ -371,6 +431,26 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val child1 = Block(
id = "CHILD1-TEXT",
fields = Block.Fields.empty(),
@ -421,10 +501,10 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(parent.id)
children = listOf(header.id, parent.id)
)
val document = listOf(page, parent, child1, child2, child3)
val document = listOf(page, header, title, parent, child1, child2, child3)
val params = UnlinkBlocks.Params(
context = root,
@ -469,9 +549,9 @@ class EditorBackspaceNestedDeleteTest : EditorPresentationTestSetup() {
ViewState.Success(
listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null
text = title.content<Block.Content.Text>().text
),
BlockView.Text.Paragraph(
id = parent.id,

View file

@ -152,7 +152,7 @@ class EditorCheckboxTest : EditorPresentationTestSetup() {
}
}
fun stubUpdateCheckbox(
private fun stubUpdateCheckbox(
payload: Payload = Payload(
context = MockDataFactory.randomUuid(),
events = emptyList()

View file

@ -20,6 +20,26 @@ import org.mockito.MockitoAnnotations
class EditorEmptySpaceInteractionTest : EditorPresentationTestSetup() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
@get:Rule
val rule = InstantTaskExecutorRule()
@ -39,19 +59,78 @@ class EditorEmptySpaceInteractionTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new paragraph on outside-clicked event if page contains only title and icon`() {
fun `should create a new paragraph on outside-clicked event if page contains only title with icon`() {
// SETUP
val child = MockDataFactory.randomUuid()
val page = MockBlockFactory.makeOnePageWithOneTextBlock(
root = root,
child = child,
style = Block.Content.Text.Style.TITLE
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id)
)
val doc = listOf(page, header, title)
stubInterceptEvents()
stubOpenDocument(page)
stubOpenDocument(doc)
stubCreateBlock(root)
val vm = buildViewModel()
vm.onStart(root)
// TESTING
vm.onOutsideClicked()
verifyBlocking(createBlock, times(1)) {
invoke(
params = eq(
CreateBlock.Params(
context = root,
target = "",
position = Position.INNER,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.P
)
)
)
)
}
}
@Test
fun `should create a new paragraph on outside-clicked event if page contains only title with icon and one non-empty paragraph`() {
// SETUP
val block = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.P,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, block.id)
)
val doc = listOf(page, header, title, block)
stubInterceptEvents()
stubOpenDocument(doc)
stubCreateBlock(root)
val vm = buildViewModel()

View file

@ -46,9 +46,7 @@ class EditorFocusTest : EditorPresentationTestSetup() {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
content = Block.Content.Smart(Block.Content.Smart.Type.PAGE),
children = listOf(block.id)
),
block

View file

@ -35,6 +35,26 @@ class EditorGranularChangeTest : EditorPresentationTestSetup() {
val delay = 1000L
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val checkbox = Block(
id = MockDataFactory.randomString(),
fields = Block.Fields.empty(),
@ -51,11 +71,13 @@ class EditorGranularChangeTest : EditorPresentationTestSetup() {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(checkbox.id)
children = listOf(header.id, checkbox.id)
),
header,
title,
checkbox
)
@ -82,8 +104,8 @@ class EditorGranularChangeTest : EditorPresentationTestSetup() {
val before = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<Block.Content.Text>().text,
isFocused = false
),
BlockView.Text.Checkbox(
@ -98,8 +120,8 @@ class EditorGranularChangeTest : EditorPresentationTestSetup() {
val after = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<Block.Content.Text>().text,
isFocused = false
),
BlockView.Text.Checkbox(

View file

@ -11,6 +11,7 @@ import com.anytypeio.anytype.domain.event.model.Event
import com.anytypeio.anytype.domain.ext.content
import com.anytypeio.anytype.presentation.MockBlockFactory
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.presentation.util.TXT
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.times
@ -61,7 +62,7 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
vm.onEndLineEnterClicked(
id = child,
text = page.last().content<Block.Content.Text>().text,
text = page.last().content<TXT>().text,
marks = emptyList()
)
@ -219,6 +220,26 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val style = Block.Content.Text.Style.CHECKBOX
val child = MockDataFactory.randomUuid()
@ -237,11 +258,13 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(child)
children = listOf(header.id, child)
),
header,
title,
checkbox
)
@ -275,8 +298,8 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val before = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<TXT>().text,
isFocused = false
),
BlockView.Text.Checkbox(
@ -316,8 +339,8 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val after = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<TXT>().text,
isFocused = false
),
BlockView.Text.Paragraph(
@ -336,6 +359,26 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val style = Block.Content.Text.Style.BULLET
val child = MockDataFactory.randomUuid()
@ -354,11 +397,13 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(child)
children = listOf(header.id, child)
),
header,
title,
checkbox
)
@ -392,8 +437,8 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val before = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<TXT>().text,
isFocused = false
),
BlockView.Text.Bulleted(
@ -432,8 +477,8 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val after = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<TXT>().text,
isFocused = false
),
BlockView.Text.Paragraph(
@ -455,6 +500,26 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val style = Block.Content.Text.Style.TOGGLE
val child = MockDataFactory.randomUuid()
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val checkbox = Block(
id = child,
fields = Block.Fields(emptyMap()),
@ -470,11 +535,13 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(child)
children = listOf(header.id, child)
),
header,
title,
checkbox
)
@ -508,8 +575,8 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val before = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<Block.Content.Text>().text,
isFocused = false
),
BlockView.Text.Toggle(
@ -549,8 +616,8 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val after = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<Block.Content.Text>().text,
isFocused = false
),
BlockView.Text.Paragraph(
@ -569,6 +636,26 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val style = Block.Content.Text.Style.NUMBERED
val child = MockDataFactory.randomUuid()
@ -587,11 +674,13 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(child)
children = listOf(header.id, child)
),
header,
title,
checkbox
)
@ -625,8 +714,8 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val before = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<Block.Content.Text>().text,
isFocused = false
),
BlockView.Text.Numbered(
@ -644,7 +733,7 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
vm.onEndLineEnterClicked(
id = child,
marks = emptyList(),
text = page.last().content<Block.Content.Text>().text
text = page.last().content<TXT>().text
)
verifyBlocking(updateTextStyle, times(1)) {
@ -666,8 +755,8 @@ class EditorListBlockTest : EditorPresentationTestSetup() {
val after = ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
text = null,
id = title.id,
text = title.content<Block.Content.Text>().text,
isFocused = false
),
BlockView.Text.Paragraph(

View file

@ -9,14 +9,15 @@ import com.anytypeio.anytype.core_ui.state.ControlPanelState
import com.anytypeio.anytype.core_ui.widgets.toolbar.adapter.Mention
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.block.model.Block
import com.anytypeio.anytype.domain.ext.content
import com.anytypeio.anytype.domain.icon.DocumentEmojiIconProvider
import com.anytypeio.anytype.domain.page.CreateNewDocument
import com.anytypeio.anytype.domain.page.navigation.GetListPages
import com.anytypeio.anytype.presentation.page.PageViewModel
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.presentation.util.TXT
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.*
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -43,6 +44,26 @@ class EditorMentionTest : EditorPresentationTestSetup() {
@Test
fun `should update text with cursor position`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val mentionTrigger = "@a"
val from = 11
val givenText = "page about $mentionTrigger music"
@ -88,10 +109,10 @@ class EditorMentionTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(a.id)
children = listOf(header.id, a.id)
)
val document = listOf(page, a)
val document = listOf(page, header, title, a)
stubOpenDocument(document)
stubInterceptEvents()
@ -139,9 +160,9 @@ class EditorMentionTest : EditorPresentationTestSetup() {
ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null,
text = title.content<TXT>().text,
mode = BlockView.Mode.EDIT
),
BlockView.Text.Paragraph(
@ -192,6 +213,27 @@ class EditorMentionTest : EditorPresentationTestSetup() {
@Test
fun `should create new page with proper name and add new mention with page id`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val mentionTrigger = "@Jazz"
val from = 11
val givenText = "page about $mentionTrigger music"
@ -224,10 +266,10 @@ class EditorMentionTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(a.id)
children = listOf(header.id, a.id)
)
val document = listOf(page, a)
val document = listOf(page, header, title, a)
stubOpenDocument(document)
stubInterceptEvents()
@ -282,10 +324,12 @@ class EditorMentionTest : EditorPresentationTestSetup() {
)
}
runBlockingTest {
verify(createNewDocument, times(1)).invoke(CreateNewDocument.Params(
name = newPageName
))
verifyBlocking(createNewDocument, times(1)) {
invoke(
CreateNewDocument.Params(
name = newPageName
)
)
}
vm.state.test().apply {
@ -293,9 +337,9 @@ class EditorMentionTest : EditorPresentationTestSetup() {
ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null,
text = title.content<TXT>().text,
mode = BlockView.Mode.EDIT
),
BlockView.Text.Paragraph(
@ -336,6 +380,27 @@ class EditorMentionTest : EditorPresentationTestSetup() {
@Test
fun `should create new page with untitled name and add new mention with page id`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val mentionTrigger = "@"
val from = 11
val givenText = "page about $mentionTrigger music"
@ -368,10 +433,10 @@ class EditorMentionTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(a.id)
children = listOf(header.id, a.id)
)
val document = listOf(page, a)
val document = listOf(page, header, title, a)
stubOpenDocument(document)
stubInterceptEvents()
@ -426,10 +491,12 @@ class EditorMentionTest : EditorPresentationTestSetup() {
)
}
runBlockingTest {
verify(createNewDocument, times(1)).invoke(CreateNewDocument.Params(
name = newPageName
))
verifyBlocking(createNewDocument, times(1)) {
invoke(
CreateNewDocument.Params(
name = newPageName
)
)
}
vm.state.test().apply {
@ -437,9 +504,9 @@ class EditorMentionTest : EditorPresentationTestSetup() {
ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null,
text = title.content<TXT>().text,
mode = BlockView.Mode.EDIT
),
BlockView.Text.Paragraph(

View file

@ -6,7 +6,6 @@ import com.anytypeio.anytype.domain.block.interactor.MergeBlocks
import com.anytypeio.anytype.domain.block.interactor.UpdateText
import com.anytypeio.anytype.domain.block.model.Block
import com.anytypeio.anytype.domain.ext.content
import com.anytypeio.anytype.presentation.MockBlockFactory
import com.anytypeio.anytype.presentation.page.PageViewModel
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.nhaarman.mockitokotlin2.eq
@ -34,22 +33,61 @@ class EditorMergeTest : EditorPresentationTestSetup() {
@Test
fun `should update text and proceed with merging the first paragraph with the second on non-empty-block-backspace-pressed event`() {
val firstChild = MockDataFactory.randomUuid()
val secondChild = MockDataFactory.randomUuid()
val thirdChild = MockDataFactory.randomUuid()
val page = MockBlockFactory.makeOnePageWithThreeTextBlocks(
root = root,
firstChild = firstChild,
secondChild = secondChild,
thirdChild = thirdChild,
firstChildStyle = Block.Content.Text.Style.TITLE,
secondChildStyle = Block.Content.Text.Style.P,
thirdChildStyle = Block.Content.Text.Style.P
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val first = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields(emptyMap()),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.P
),
children = emptyList()
)
val second = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields(emptyMap()),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.P
),
children = emptyList()
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, first.id, second.id)
)
val doc = listOf(page, header, title, first, second)
stubInterceptEvents()
stubOpenDocument(page)
stubOpenDocument(doc)
stubMergeBlocks(root)
stubUpdateText()
@ -58,20 +96,20 @@ class EditorMergeTest : EditorPresentationTestSetup() {
vm.onStart(root)
vm.onBlockFocusChanged(
id = thirdChild,
id = second.id,
hasFocus = true
)
val text = MockDataFactory.randomString()
vm.onTextChanged(
id = thirdChild,
id = second.id,
marks = emptyList(),
text = text
)
vm.onNonEmptyBlockBackspaceClicked(
id = thirdChild,
id = second.id,
marks = emptyList(),
text = text
)
@ -85,7 +123,7 @@ class EditorMergeTest : EditorPresentationTestSetup() {
context = root,
text = text,
marks = emptyList(),
target = thirdChild
target = second.id
)
)
)
@ -96,7 +134,7 @@ class EditorMergeTest : EditorPresentationTestSetup() {
params = eq(
MergeBlocks.Params(
context = root,
pair = Pair(secondChild, thirdChild)
pair = Pair(first.id, second.id)
)
)
)

View file

@ -13,6 +13,7 @@ import com.anytypeio.anytype.domain.ext.content
import com.anytypeio.anytype.presentation.page.PageViewModel.Companion.DELAY_REFRESH_DOCUMENT_TO_ENTER_MULTI_SELECT_MODE
import com.anytypeio.anytype.presentation.page.PageViewModel.Companion.TEXT_CHANGES_DEBOUNCE_DURATION
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.presentation.util.TXT
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verifyBlocking
@ -39,6 +40,26 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val a = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
@ -56,10 +77,10 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(a.id)
children = listOf(header.id, a.id)
)
val document = listOf(page, a)
val document = listOf(page, header, title, a)
stubOpenDocument(document)
stubInterceptEvents()
@ -130,9 +151,9 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null,
text = title.content<TXT>().text,
mode = BlockView.Mode.READ
),
BlockView.Text.Numbered(
@ -164,9 +185,9 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null,
text = title.content<TXT>().text,
mode = BlockView.Mode.READ
),
BlockView.Text.Numbered(
@ -228,9 +249,9 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
ViewState.Success(
blocks = listOf(
BlockView.Title.Document(
id = root,
id = title.id,
isFocused = false,
text = null,
text = title.content<TXT>().text,
mode = BlockView.Mode.READ
),
BlockView.Text.Highlight(
@ -397,6 +418,26 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
// SETUP
val ttl = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(ttl.id)
)
val grandchild1 = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
@ -458,10 +499,10 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(parent.id)
children = listOf(header.id, parent.id)
)
val document = listOf(page, parent, child1, child2, grandchild1, grandchild2)
val document = listOf(page, header, ttl, parent, child1, child2, grandchild1, grandchild2)
stubOpenDocument(document)
stubInterceptEvents()
@ -484,9 +525,9 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
coroutineTestRule.advanceTime(DELAY_REFRESH_DOCUMENT_TO_ENTER_MULTI_SELECT_MODE)
val title = BlockView.Title.Document(
id = root,
id = ttl.id,
isFocused = false,
text = null,
text = ttl.content<TXT>().text,
mode = BlockView.Mode.READ
)
@ -614,6 +655,26 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val child1 = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
@ -653,10 +714,10 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(parent.id)
children = listOf(header.id, parent.id)
)
val document = listOf(page, parent, child1, child2)
val document = listOf(page, header, title, parent, child1, child2)
stubOpenDocument(document)
stubInterceptEvents()
@ -678,10 +739,10 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
coroutineTestRule.advanceTime(DELAY_REFRESH_DOCUMENT_TO_ENTER_MULTI_SELECT_MODE)
val title = BlockView.Title.Document(
id = root,
val titleView = BlockView.Title.Document(
id = title.id,
isFocused = false,
text = null,
text = title.content<TXT>().text,
mode = BlockView.Mode.READ
)
@ -722,7 +783,7 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
assertValue(
ViewState.Success(
blocks = listOf(
title,
titleView,
parentView,
child1View,
child2View
@ -743,7 +804,7 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
assertValue(
ViewState.Success(
blocks = listOf(
title,
titleView,
parentView.copy(isSelected = true),
child1View.copy(isSelected = true),
child2View.copy(isSelected = true)
@ -764,7 +825,7 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
assertValue(
ViewState.Success(
blocks = listOf(
title,
titleView,
parentView.copy(isSelected = true),
child1View.copy(isSelected = true),
child2View.copy(isSelected = true)
@ -781,6 +842,26 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val grandchild1 = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
@ -842,10 +923,10 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(parent.id)
children = listOf(header.id, parent.id)
)
val document = listOf(page, parent, child1, child2, grandchild1, grandchild2)
val document = listOf(page, header, title, parent, child1, child2, grandchild1, grandchild2)
stubOpenDocument(document)
stubInterceptEvents()
@ -887,6 +968,81 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
clearPendingCoroutines()
}
@Test
fun `should not select title when trying to select all blocks`() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val a = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.NUMBERED
)
)
val focus = listOf(a.id, title.id).random()
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, a.id)
)
val document = listOf(page, header, title, a)
stubOpenDocument(document)
stubInterceptEvents()
val vm = buildViewModel()
vm.onStart(root)
// TESTING
vm.apply {
onBlockFocusChanged(id = focus, hasFocus = true)
onEnterMultiSelectModeClicked()
onMultiSelectModeSelectAllClicked()
}
coroutineTestRule.advanceTime(DELAY_REFRESH_DOCUMENT_TO_ENTER_MULTI_SELECT_MODE)
vm.controlPanelViewState.test().assertValue { state ->
state.multiSelect.isVisible
&& !state.multiSelect.isScrollAndMoveEnabled
&& state.multiSelect.count == 1
}
clearPendingCoroutines()
}
private fun clearPendingCoroutines() {
coroutineTestRule.advanceTime(TEXT_CHANGES_DEBOUNCE_DURATION)

View file

@ -26,6 +26,7 @@ import com.anytypeio.anytype.presentation.page.PageViewModel
import com.anytypeio.anytype.presentation.page.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.page.selection.SelectionStateHolder
import com.anytypeio.anytype.presentation.page.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.util.Bridge
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.stub
@ -200,7 +201,8 @@ open class EditorPresentationTestSetup {
move = move,
turnIntoDocument = turnIntoDocument,
analytics = analytics
)
),
bridge = Bridge()
)
}

View file

@ -32,6 +32,26 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
MockitoAnnotations.initMocks(this)
}
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
@Test
fun `should create a new text block after title with INNER position if document has only title block`() {
@ -43,19 +63,19 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf()
children = listOf(header.id)
)
val style = Block.Content.Text.Style.values().random()
val params = CreateBlock.Params(
context = root,
target = root,
position = Position.INNER,
target = title.id,
position = Position.BOTTOM,
prototype = Block.Prototype.Text(style)
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -69,7 +89,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddTextBlockClicked(style)
@ -79,7 +99,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new text block at the TOP of the document's first block`() {
fun `should create a new text block below title`() {
// SETUP
@ -100,19 +120,19 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(block.id)
children = listOf(header.id, block.id)
)
val style = Block.Content.Text.Style.values().random()
val params = CreateBlock.Params(
context = root,
target = block.id,
position = Position.TOP,
target = title.id,
position = Position.BOTTOM,
prototype = Block.Prototype.Text(style)
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -126,7 +146,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddTextBlockClicked(style)
@ -136,7 +156,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new document after title with INNER position if document has only title block`() {
fun `should create a new document after title if document has only title block`() {
// SETUP
@ -146,16 +166,16 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf()
children = listOf(header.id)
)
val params = CreateDocument.Params(
context = root,
target = root,
position = Position.INNER
target = title.id,
position = Position.BOTTOM
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -168,7 +188,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddNewPageClicked()
@ -178,7 +198,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new document at the TOP of the document's first block`() {
fun `should create a new document below document title`() {
// SETUP
@ -199,16 +219,16 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(block.id)
children = listOf(header.id, block.id)
)
val params = CreateDocument.Params(
context = root,
target = block.id,
position = Position.TOP
target = title.id,
position = Position.BOTTOM
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -221,7 +241,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddNewPageClicked()
@ -231,7 +251,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new file block after title with INNER position if document has only title block`() {
fun `should create a new file block below title if document has only title block`() {
// SETUP
@ -241,7 +261,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf()
children = listOf(header.id)
)
val types = Block.Content.File.Type.values().filter { it != Block.Content.File.Type.NONE }
@ -250,15 +270,15 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
val params = CreateBlock.Params(
context = root,
target = root,
position = Position.INNER,
target = title.id,
position = Position.BOTTOM,
prototype = Block.Prototype.File(
type = type,
state = Block.Content.File.State.EMPTY
)
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -271,7 +291,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddFileBlockClicked(type = type)
@ -281,7 +301,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new file block at the TOP of the document's first block`() {
fun `should create a new file block below document title`() {
// SETUP
@ -302,7 +322,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(block.id)
children = listOf(header.id, block.id)
)
val types = Block.Content.File.Type.values().filter { it != Block.Content.File.Type.NONE }
@ -311,15 +331,15 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
val params = CreateBlock.Params(
context = root,
target = block.id,
position = Position.TOP,
target = title.id,
position = Position.BOTTOM,
prototype = Block.Prototype.File(
type = type,
state = Block.Content.File.State.EMPTY
)
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -332,7 +352,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddFileBlockClicked(type = type)
@ -352,17 +372,17 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf()
children = listOf(header.id)
)
val params = CreateBlock.Params(
context = root,
target = root,
position = Position.INNER,
target = title.id,
position = Position.BOTTOM,
prototype = Block.Prototype.Bookmark
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -375,7 +395,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddBookmarkBlockClicked()
@ -385,7 +405,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new bookmark block at the TOP of the document's first block`() {
fun `should create a new bookmark block below title block`() {
// SETUP
@ -406,17 +426,17 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(block.id)
children = listOf(header.id, block.id)
)
val params = CreateBlock.Params(
context = root,
target = block.id,
position = Position.TOP,
target = title.id,
position = Position.BOTTOM,
prototype = Block.Prototype.Bookmark
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -429,7 +449,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddBookmarkBlockClicked()
@ -439,7 +459,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new divider block after title with INNER position if document has only title block`() {
fun `should create a new divider block after title if document has only title block`() {
// SETUP
@ -449,17 +469,17 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf()
children = listOf(header.id)
)
val params = CreateBlock.Params(
context = root,
target = root,
position = Position.INNER,
target = title.id,
position = Position.BOTTOM,
prototype = Block.Prototype.Divider
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -472,7 +492,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddDividerBlockClicked()
@ -482,7 +502,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
}
@Test
fun `should create a new divider block at the TOP of the document's first block`() {
fun `should create a new divider block after document title`() {
// SETUP
@ -503,17 +523,17 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(block.id)
children = listOf(header.id, block.id)
)
val params = CreateBlock.Params(
context = root,
target = block.id,
position = Position.TOP,
target = title.id,
position = Position.BOTTOM,
prototype = Block.Prototype.Divider
)
val document = listOf(page)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
@ -526,7 +546,7 @@ class EditorTitleAddBlockTest : EditorPresentationTestSetup() {
vm.apply {
onStart(root)
onBlockFocusChanged(
id = root,
id = title.id,
hasFocus = true
)
onAddDividerBlockClicked()

View file

@ -0,0 +1,171 @@
package com.anytypeio.anytype.presentation.page.editor
import MockDataFactory
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_ui.state.ControlPanelState
import com.anytypeio.anytype.domain.block.model.Block
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.presentation.page.PageViewModel
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.jraska.livedata.test
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
class EditorTitleTest : EditorPresentationTestSetup() {
@get:Rule
val rule = InstantTaskExecutorRule()
@get:Rule
val coroutineTestRule = CoroutinesTestRule()
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
@Test
fun `should not open action menu for title block`() {
// SETUP
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id)
)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
val vm = buildViewModel()
// TESTING
val toasts = mutableListOf<String>()
runBlockingTest {
val toastSubscription = launch { vm.toasts.collect { toasts.add(it) } }
val commandTestObserver = vm.commands.test()
vm.apply {
onStart(root)
onBlockFocusChanged(title.id, true)
onBlockToolbarBlockActionsClicked()
}
commandTestObserver.assertNoValue().assertHistorySize(0)
assertEquals(
expected = 1,
actual = toasts.size
)
assertEquals(
expected = PageViewModel.CANNOT_OPEN_ACTION_MENU_FOR_TITLE_ERROR,
actual = toasts.first()
)
toastSubscription.cancel()
}
}
@Test
fun `should not open style panel for title block`() {
// SETUP
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id)
)
val document = listOf(page, header, title)
stubOpenDocument(document = document)
stubInterceptEvents(InterceptEvents.Params(context = root))
val vm = buildViewModel()
// TESTING
val toasts = mutableListOf<String>()
runBlockingTest {
val toastSubscription = launch { vm.toasts.collect { toasts.add(it) } }
val commandTestObserver = vm.commands.test()
val controlPanelObserver = vm.controlPanelViewState.test()
vm.apply {
onStart(root)
onBlockFocusChanged(title.id, true)
onBlockToolbarStyleClicked()
}
commandTestObserver.assertNoValue().assertHistorySize(0)
controlPanelObserver.assertValue(
ControlPanelState(
mainToolbar = ControlPanelState.Toolbar.Main(
isVisible = true
),
stylingToolbar = ControlPanelState.Toolbar.Styling.reset(),
multiSelect = ControlPanelState.Toolbar.MultiSelect(
isVisible = false,
isScrollAndMoveEnabled = false
),
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar.reset(),
navigationToolbar = ControlPanelState.Toolbar.Navigation(isVisible = false)
)
)
assertEquals(
expected = 1,
actual = toasts.size
)
assertEquals(
expected = PageViewModel.CANNOT_OPEN_STYLE_PANEL_FOR_TITLE_ERROR,
actual = toasts.first()
)
toastSubscription.cancel()
}
}
}

View file

@ -0,0 +1,5 @@
package com.anytypeio.anytype.presentation.util
import com.anytypeio.anytype.domain.block.model.Block
typealias TXT = Block.Content.Text

View file

@ -457,6 +457,8 @@ message Rpc {
TOP = 1;
// new block will be created as the first children of existing
INNER = 2;
// new block will be created after header (not required for set at client side, will auto set for title block)
TITLE = 3;
}
}
@ -736,6 +738,7 @@ message Rpc {
}
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -812,6 +815,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -2234,6 +2238,94 @@ message Rpc {
}
}
message History {
// returns list of versions (changes)
message Versions {
message Version {
string id = 1;
repeated string previousIds = 2;
string authorId = 3;
string authorName = 4;
int64 time = 5;
int64 groupId = 6;
}
message Request {
string pageId = 1;
// when indicated, results will include versions before given id
string lastVersionId = 2;
// desired count of versions
int32 limit = 3;
}
message Response {
Error error = 1;
repeated Version versions = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
// returns blockShow event for given version
message Show {
message Request {
string pageId = 1;
string versionId = 2;
}
message Response {
Error error = 1;
Event.Block.Show blockShow = 2;
History.Versions.Version version = 3;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
message SetVersion {
message Request {
string pageId = 1;
string versionId = 2;
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
}
message Page {
message Create {
message Request {

View file

@ -69,6 +69,7 @@ message Block {
Row = 0;
Column = 1;
Div = 2;
Header = 3;
}
}