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

Refactor event handler (#201)

This commit is contained in:
Evgenii Kozlov 2020-02-08 23:28:08 +03:00 committed by GitHub
parent 7ed1181533
commit 48159c24af
39 changed files with 829 additions and 1080 deletions

View file

@ -7,9 +7,9 @@ import com.agileburo.anytype.domain.block.interactor.DragAndDrop
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.config.GetConfig
import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard
import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard
import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard
import com.agileburo.anytype.domain.event.interactor.ObserveEvents
import com.agileburo.anytype.domain.event.interactor.EventChannel
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.image.ImageLoader
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.domain.page.CreatePage
@ -48,9 +48,8 @@ class HomeDashboardModule {
createPage: CreatePage,
closeDashboard: CloseDashboard,
getConfig: GetConfig,
observeHomeDashboard: ObserveHomeDashboard,
dnd: DragAndDrop,
observeEvents: ObserveEvents
interceptEvents: InterceptEvents
): HomeDashboardViewModelFactory = HomeDashboardViewModelFactory(
getCurrentAccount = getCurrentAccount,
loadImage = loadImage,
@ -58,9 +57,8 @@ class HomeDashboardModule {
createPage = createPage,
closeDashboard = closeDashboard,
getConfig = getConfig,
observeHomeDashboard = observeHomeDashboard,
dnd = dnd,
observeEvents = observeEvents
interceptEvents = interceptEvents
)
@Provides
@ -112,15 +110,6 @@ class HomeDashboardModule {
repo = repo
)
@Provides
@PerScreen
fun provideObserveHomeDashboardUseCase(
repo: BlockRepository
): ObserveHomeDashboard = ObserveHomeDashboard(
context = Dispatchers.IO,
repo = repo
)
@Provides
@PerScreen
fun provideDragAndDropUseCase(
@ -131,10 +120,10 @@ class HomeDashboardModule {
@Provides
@PerScreen
fun provideObserveEventsUseCase(
repo: BlockRepository
): ObserveEvents = ObserveEvents(
fun provideInterceptEvents(
channel: EventChannel
): InterceptEvents = InterceptEvents(
context = Dispatchers.IO,
repo = repo
channel = channel
)
}

View file

@ -6,7 +6,6 @@ import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.event.interactor.EventChannel
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.page.ClosePage
import com.agileburo.anytype.domain.page.ObservePage
import com.agileburo.anytype.domain.page.OpenPage
import com.agileburo.anytype.presentation.page.PageViewModelFactory
import com.agileburo.anytype.ui.page.PageFragment
@ -69,14 +68,6 @@ class PageModule {
repo = repo
)
@Provides
@PerScreen
fun provideObservePageUseCase(
repo: BlockRepository
): ObservePage = ObservePage(
repo = repo
)
@Provides
@PerScreen
fun provideClosePageUseCase(

View file

@ -143,14 +143,8 @@ class DataModule {
@Provides
@Singleton
fun provideBlockRemote(
middleware: Middleware,
eventProxy: EventProxy
): BlockRemote {
return BlockMiddleware(
middleware = middleware,
events = eventProxy
)
}
middleware: Middleware
): BlockRemote = BlockMiddleware(middleware = middleware)
@Provides
@Singleton

View file

@ -12,8 +12,8 @@ import com.agileburo.anytype.core_utils.ext.invisible
import com.agileburo.anytype.core_utils.ext.toast
import com.agileburo.anytype.core_utils.ext.visible
import com.agileburo.anytype.di.common.componentManager
import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.State
import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel
import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel.Machine.State
import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModelFactory
import com.agileburo.anytype.presentation.mapper.toView
import com.agileburo.anytype.presentation.profile.ProfileView
@ -92,10 +92,10 @@ class HomeDashboardFragment : ViewStateFragment<State>(R.layout.fragment_desktop
progress.invisible()
requireActivity().toast("Error: ${state.error}")
}
state.homeDashboard != null -> {
state.dashboard != null -> {
progress.invisible()
fab.visible()
dashboardAdapter.update(state.homeDashboard!!.toView())
state.dashboard?.let { dashboardAdapter.update(it.toView()) }
}
}
}

View file

@ -20,20 +20,20 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@id/greeting"
android:id="@+id/greeting"
android:fontFamily="@font/graphik_semibold"
android:scaleX="1.0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="16dp"
android:textColor="@color/white"
android:textSize="36sp"
android:textStyle="bold"
android:scaleX="1.0"
android:scaleY="1.0"
android:textColor="@color/white"
android:textSize="34sp"
app:layout_constraintBottom_toBottomOf="@+id/avatar"
app:layout_constraintEnd_toStartOf="@+id/avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="@+id/avatar" />
<Constraint
android:id="@+id/avatar"
android:layout_width="36dp"
@ -54,18 +54,20 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@id/greeting"
android:id="@+id/greeting"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/graphik_semibold"
android:scaleX="0.0"
android:scaleY="0.0"
android:textColor="@color/white"
android:textSize="36sp"
android:textStyle="bold"
android:textSize="34sp"
app:layout_constraintBottom_toBottomOf="@+id/avatar"
app:layout_constraintEnd_toStartOf="@+id/avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="@+id/avatar" />
<Constraint
android:id="@+id/avatar"
android:layout_width="36dp"

View file

@ -1,10 +1,13 @@
package com.agileburo.anytype.data.auth.event
import com.agileburo.anytype.data.auth.mapper.toDomain
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.interactor.EventChannel
import kotlinx.coroutines.flow.map
class EventDataChannel(private val remote: EventRemoteChannel) : EventChannel {
override fun observeEvents() =
remote.observeEvents().map { events -> events.map { it.toDomain() } }
override fun observeEvents(
context: Id?
) = remote.observeEvents(context).map { events -> events.map { it.toDomain() } }
}

View file

@ -4,5 +4,5 @@ import com.agileburo.anytype.data.auth.model.EventEntity
import kotlinx.coroutines.flow.Flow
interface EventRemoteChannel {
fun observeEvents(): Flow<List<EventEntity>>
fun observeEvents(context: String? = null): Flow<List<EventEntity>>
}

View file

@ -202,7 +202,7 @@ fun Block.Content.Text.Mark.toEntity(): BlockEntity.Content.Text.Mark {
fun ConfigEntity.toDomain(): Config {
return Config(
homeDashboardId = homeId
home = homeId
)
}
@ -272,18 +272,21 @@ fun EventEntity.toDomain(): Event {
is EventEntity.Command.ShowBlock -> {
Event.Command.ShowBlock(
rootId = rootId,
blocks = blocks.map { it.toDomain() }
blocks = blocks.map { it.toDomain() },
context = context
)
}
is EventEntity.Command.AddBlock -> {
Event.Command.AddBlock(
blocks = blocks.map { it.toDomain() }
blocks = blocks.map { it.toDomain() },
context = context
)
}
is EventEntity.Command.UpdateBlockText -> {
Event.Command.UpdateBlockText(
id = id,
text = text
text = text,
context = context
)
}
is EventEntity.Command.UpdateStructure -> {
@ -295,11 +298,13 @@ fun EventEntity.toDomain(): Event {
}
is EventEntity.Command.DeleteBlock -> {
Event.Command.DeleteBlock(
context = context,
target = target
)
}
is EventEntity.Command.GranularChange -> {
Event.Command.GranularChange(
context = context,
id = id,
text = text,
style = if (style != null)
@ -309,6 +314,14 @@ fun EventEntity.toDomain(): Event {
color = color
)
}
is EventEntity.Command.LinkGranularChange -> {
Event.Command.LinkGranularChange(
context = context,
id = id,
target = target,
fields = fields?.let { Block.Fields(it.map) }
)
}
}
}

View file

@ -2,36 +2,50 @@ package com.agileburo.anytype.data.auth.model
sealed class EventEntity {
abstract val context: String
sealed class Command : EventEntity() {
data class ShowBlock(
override val context: String,
val rootId: String,
val blocks: List<BlockEntity>
) : Command()
data class AddBlock(
override val context: String,
val blocks: List<BlockEntity>
) : Command()
data class UpdateBlockText(
override val context: String,
val id: String,
val text: String
) : Command()
data class GranularChange(
override val context: String,
val id: String,
val text: String? = null,
val style: BlockEntity.Content.Text.Style? = null,
val color: String? = null
) : Command()
data class LinkGranularChange(
override val context: String,
val id: String,
val target: String,
val fields: BlockEntity.Fields?
) : Command()
data class UpdateStructure(
val context: String,
override val context: String,
val id: String,
val children: List<String>
) : Command()
data class DeleteBlock(
override val context: String,
val target: String
) : Command()
}

View file

@ -4,7 +4,6 @@ import com.agileburo.anytype.data.auth.mapper.toDomain
import com.agileburo.anytype.data.auth.mapper.toEntity
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import kotlinx.coroutines.flow.map
class BlockDataRepository(
private val factory: BlockDataStoreFactory
@ -30,13 +29,6 @@ class BlockDataRepository(
factory.remote.closePage(id)
}
override fun observeBlocks() =
factory.remote.observeBlocks().map { blocks -> blocks.map { it.toDomain() } }
override fun observeEvents() = factory.remote.observeEvents().map { it.toDomain() }
override fun observePages() =
factory.remote.observePages().map { blocks -> blocks.map { it.toDomain() } }
override suspend fun updateText(command: Command.UpdateText) {
factory.remote.updateText(command.toEntity())
}

View file

@ -1,11 +1,8 @@
package com.agileburo.anytype.data.auth.repo.block
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.CommandEntity
import com.agileburo.anytype.data.auth.model.ConfigEntity
import com.agileburo.anytype.data.auth.model.EventEntity
import com.agileburo.anytype.domain.common.Id
import kotlinx.coroutines.flow.Flow
interface BlockDataStore {
suspend fun create(command: CommandEntity.Create)
@ -22,8 +19,4 @@ interface BlockDataStore {
suspend fun closePage(id: String)
suspend fun openDashboard(contextId: String, id: String)
suspend fun closeDashboard(id: String)
fun observeBlocks(): Flow<List<BlockEntity>>
fun observeEvents(): Flow<EventEntity>
fun observePages(): Flow<List<BlockEntity>>
}

View file

@ -1,11 +1,8 @@
package com.agileburo.anytype.data.auth.repo.block
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.CommandEntity
import com.agileburo.anytype.data.auth.model.ConfigEntity
import com.agileburo.anytype.data.auth.model.EventEntity
import com.agileburo.anytype.domain.common.Id
import kotlinx.coroutines.flow.Flow
interface BlockRemote {
suspend fun create(command: CommandEntity.Create)
@ -20,11 +17,6 @@ interface BlockRemote {
suspend fun createPage(parentId: String): String
suspend fun openPage(id: String)
suspend fun closePage(id: String)
fun observeBlocks(): Flow<List<BlockEntity>>
fun observeEvents(): Flow<EventEntity>
fun observePages(): Flow<List<BlockEntity>>
suspend fun openDashboard(contextId: String, id: String)
suspend fun closeDashboard(id: String)
}

View file

@ -14,10 +14,6 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
remote.closeDashboard(id = id)
}
override fun observeBlocks() = remote.observeBlocks()
override fun observePages() = remote.observePages()
override fun observeEvents() = remote.observeEvents()
override suspend fun createPage(parentId: String): String = remote.createPage(parentId)
override suspend fun openPage(id: String) {
remote.openPage(id)

View file

@ -1,11 +1,8 @@
package com.agileburo.anytype.domain.block.repo
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.event.model.Event
import kotlinx.coroutines.flow.Flow
interface BlockRepository {
suspend fun dnd(command: Command.Dnd)
@ -22,11 +19,4 @@ interface BlockRepository {
suspend fun closePage(id: String)
suspend fun openDashboard(contextId: String, id: String)
suspend fun closeDashboard(id: String)
@Deprecated("Will be removed and replaced by observeEvents()")
fun observeBlocks(): Flow<List<Block>>
@Deprecated("Will be removed and replaced by [EventChannel]")
fun observeEvents(): Flow<Event>
fun observePages(): Flow<List<Block>>
}

View file

@ -1,3 +1,9 @@
package com.agileburo.anytype.domain.config
data class Config(val homeDashboardId: String)
import com.agileburo.anytype.domain.common.Id
/**
* Anytype app configuration properties.
* @property home id of the home dashboard
*/
data class Config(val home: Id)

View file

@ -17,7 +17,7 @@ class CloseDashboard(
override suspend fun run(params: Param) = try {
if (params.id == MainConfig.HOME_DASHBOARD_ID)
repo.getConfig().let { config ->
repo.closeDashboard(id = config.homeDashboardId)
repo.closeDashboard(id = config.home)
}.let {
Either.Right(it)
}

View file

@ -1,38 +0,0 @@
package com.agileburo.anytype.domain.dashboard.interactor
import com.agileburo.anytype.domain.base.FlowUseCase
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.dashboard.model.HomeDashboard
import com.agileburo.anytype.domain.event.model.Event
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlin.coroutines.CoroutineContext
class ObserveHomeDashboard(
private val context: CoroutineContext,
private val repo: BlockRepository
) : FlowUseCase<HomeDashboard, ObserveHomeDashboard.Param>() {
override fun build(params: Param?) = repo
.observeEvents()
.filter { it is Event.Command.ShowBlock }
.map { it as Event.Command.ShowBlock }
.filter { isDashboardBlock(it) }
.map { event -> event.blocks.toHomeDashboard(event.rootId) }
.flowOn(context)
private fun isDashboardBlock(
event: Event.Command.ShowBlock
): Boolean {
val target = event.blocks.find { it.id == event.rootId }
if (target != null)
return target.content is Block.Content.Dashboard
else
throw IllegalStateException("Could not found any block corresponding to the root id")
}
data class Param(val id: String)
}

View file

@ -24,8 +24,8 @@ class OpenDashboard(
else {
repo.getConfig().let { config ->
repo.openDashboard(
contextId = config.homeDashboardId,
id = config.homeDashboardId
contextId = config.home,
id = config.home
).let {
Either.Right(it)
}

View file

@ -1,8 +1,9 @@
package com.agileburo.anytype.domain.event.interactor
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Event
import kotlinx.coroutines.flow.Flow
interface EventChannel {
fun observeEvents(): Flow<List<Event>>
fun observeEvents(context: Id? = null): Flow<List<Event>>
}

View file

@ -1,7 +1,7 @@
package com.agileburo.anytype.domain.event.interactor
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.FlowUseCase
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Event
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
@ -15,9 +15,15 @@ import kotlin.coroutines.CoroutineContext
class InterceptEvents(
private val context: CoroutineContext,
private val channel: EventChannel
) : FlowUseCase<List<Event>, BaseUseCase.None>() {
) : FlowUseCase<List<Event>, InterceptEvents.Params>() {
override fun build(params: BaseUseCase.None?): Flow<List<Event>> {
return channel.observeEvents().flowOn(context)
override fun build(params: Params?): Flow<List<Event>> {
return channel.observeEvents(params?.context).flowOn(context)
}
/**
* @property context optional event's context used for filtering.
* If a context is provided, only events related to this context will be intercepted.
*/
data class Params(val context: Id? = null)
}

View file

@ -1,17 +0,0 @@
package com.agileburo.anytype.domain.event.interactor
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.FlowUseCase
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.event.model.Event
import kotlinx.coroutines.flow.flowOn
import kotlin.coroutines.CoroutineContext
@Deprecated("Should use InterceptEvents")
class ObserveEvents(
private val context: CoroutineContext,
private val repo: BlockRepository
) : FlowUseCase<Event, BaseUseCase.None>() {
override fun build(params: BaseUseCase.None?) = repo.observeEvents().flowOn(context)
}

View file

@ -6,22 +6,28 @@ import com.agileburo.anytype.domain.common.Id
sealed class Event {
abstract val context: Id
sealed class Command : Event() {
data class ShowBlock(
override val context: String,
val rootId: Id,
val blocks: List<Block>
) : Command()
data class AddBlock(
override val context: String,
val blocks: List<Block>
) : Command()
data class DeleteBlock(
override val context: String,
val target: Id
) : Command()
data class UpdateBlockText(
override val context: String,
val id: Id,
val text: String
) : Command()
@ -31,9 +37,10 @@ sealed class Event {
* @property id id of the target block
* @property text new text (considered updated if not null)
* @property style new style (considered updated if not null)
* @property color new color of the whole block
* @property color new color of the whole block (considered updated if not null)
*/
data class GranularChange(
override val context: String,
val id: Id,
val text: String? = null,
val style: Text.Style? = null,
@ -42,6 +49,20 @@ sealed class Event {
fun onlyTextChanged() = style == null && color == null && text != null
}
/**
* Command to update link.
* @property context update's context
* @property id id of the link
* @property target id of the linked block
* @property fields link's fields (considered update if not null)
*/
data class LinkGranularChange(
override val context: String,
val id: Id,
val target: Id,
val fields: Block.Fields?
) : Command()
/**
* Command to update a block structure.
* @property context context id for this command (i.e page id, dashboard id, etc.)
@ -49,7 +70,7 @@ sealed class Event {
* @property children list of children ids for this block [id]
*/
data class UpdateStructure(
val context: String,
override val context: String,
val id: Id,
val children: List<Id>
) : Command()

View file

@ -17,7 +17,7 @@ class CreatePage(
override suspend fun run(params: Params) = try {
if (params.id == MainConfig.HOME_DASHBOARD_ID) {
repo.getConfig().let { config ->
repo.createPage(config.homeDashboardId).let {
repo.createPage(config.home).let {
Either.Right(it)
}
}

View file

@ -1,12 +0,0 @@
package com.agileburo.anytype.domain.page
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.FlowUseCase
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.repo.BlockRepository
class ObservePage(private val repo: BlockRepository) :
FlowUseCase<List<Block>, BaseUseCase.None>() {
override fun build(params: BaseUseCase.None?) = repo.observePages()
}

View file

@ -1,172 +0,0 @@
package com.agileburo.anytype.domain.dashboard
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.CoroutineTestRule
import com.agileburo.anytype.domain.common.MockDataFactory
import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard
import com.agileburo.anytype.domain.dashboard.model.HomeDashboard
import com.agileburo.anytype.domain.event.model.Event
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.stub
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import kotlin.test.assertTrue
class ObserveHomeDashboardTest {
@ExperimentalCoroutinesApi
@get:Rule
var rule = CoroutineTestRule()
lateinit var useCase: ObserveHomeDashboard
@Mock
lateinit var repo: BlockRepository
@Before
fun before() {
MockitoAnnotations.initMocks(this)
useCase = ObserveHomeDashboard(
context = TestCoroutineDispatcher(),
repo = repo
)
}
@Test
fun `should ignore other events`() = runBlockingTest {
val id = MockDataFactory.randomUuid()
val param = ObserveHomeDashboard.Param(id = id)
val events = flowOf(
Event.Command.UpdateBlockText(
id = MockDataFactory.randomUuid(),
text = MockDataFactory.randomString()
)
)
stubObserveEvents(events)
val result = mutableListOf<HomeDashboard>()
useCase.build(param).toList(result)
assertTrue { result.isEmpty() }
}
@Test
fun `should process event and map it to home dashboard`() = runBlockingTest {
val page = Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
content = Block.Content.Page(
style = Block.Content.Page.Style.EMPTY
),
fields = Block.Fields(
map = mapOf("name" to MockDataFactory.randomString())
)
)
val dashboard = Block(
id = MockDataFactory.randomUuid(),
children = listOf(page.id),
content = Block.Content.Dashboard(
type = Block.Content.Dashboard.Type.MAIN_SCREEN
),
fields = Block.Fields(
map = mapOf("name" to MockDataFactory.randomString())
)
)
val event = Event.Command.ShowBlock(
rootId = dashboard.id,
blocks = listOf(dashboard, page)
)
val param = ObserveHomeDashboard.Param(
id = dashboard.id
)
val flow = flowOf(event)
stubObserveEvents(flow)
val result = mutableListOf<HomeDashboard>()
val expected = HomeDashboard(
id = dashboard.id,
fields = dashboard.fields,
children = dashboard.children,
blocks = listOf(page),
type = dashboard.content.asDashboard().type
)
useCase.build(param).toList(result)
assertTrue {
result.first() == expected
}
}
@Test
fun `should ignore events not related to dashboard`() = runBlockingTest {
val title = Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.P
),
fields = Block.Fields.empty()
)
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(title.id),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
fields = Block.Fields(
map = mapOf("name" to MockDataFactory.randomString())
)
)
val event = Event.Command.ShowBlock(
rootId = page.id,
blocks = listOf(page, title)
)
val param = ObserveHomeDashboard.Param(
id = page.id
)
val flow = flowOf(event)
stubObserveEvents(flow)
val result = mutableListOf<HomeDashboard>()
useCase.build(param).toList(result)
assertTrue { result.isEmpty() }
}
private fun stubObserveEvents(events: Flow<Event>) {
repo.stub {
onBlocking { observeEvents() } doReturn events
}
}
}

View file

@ -51,7 +51,7 @@ class OpenDashboardTest {
fun `should open a home dashboard if there are no params`() = runBlockingTest {
val config = Config(
homeDashboardId = MockDataFactory.randomUuid()
home = MockDataFactory.randomUuid()
)
repo.stub {
@ -62,8 +62,8 @@ class OpenDashboardTest {
verify(repo, times(1)).getConfig()
verify(repo, times(1)).openDashboard(
contextId = config.homeDashboardId,
id = config.homeDashboardId
contextId = config.home,
id = config.home
)
verifyNoMoreInteractions(repo)
}

View file

@ -6,6 +6,7 @@ import anytype.model.Models.Account
import anytype.model.Models.Block
import com.agileburo.anytype.data.auth.model.AccountEntity
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.google.protobuf.Struct
import com.google.protobuf.Value
@ -93,6 +94,16 @@ fun Block.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result ->
}
}
fun Struct.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result ->
fieldsMap.forEach { (key, value) ->
result.map[key] = when (val case = value.kindCase) {
Value.KindCase.NUMBER_VALUE -> value.numberValue
Value.KindCase.STRING_VALUE -> value.stringValue
else -> throw IllegalStateException("$case is not supported.")
}
}
}
fun Block.dashboard(): BlockEntity.Content.Dashboard = BlockEntity.Content.Dashboard(
type = when {
dashboard.style == Block.Content.Dashboard.Style.Archive -> {

View file

@ -1,292 +1,21 @@
package com.agileburo.anytype.middleware.block
import anytype.Events
import anytype.model.Models
import anytype.model.Models.Block.Content.Dashboard
import anytype.model.Models.Block.Content.Page
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.CommandEntity
import com.agileburo.anytype.data.auth.model.ConfigEntity
import com.agileburo.anytype.data.auth.model.EventEntity
import com.agileburo.anytype.data.auth.repo.block.BlockRemote
import com.agileburo.anytype.middleware.EventProxy
import com.agileburo.anytype.middleware.interactor.Middleware
import com.agileburo.anytype.middleware.link
import com.agileburo.anytype.middleware.toMiddleware
import com.google.protobuf.Value
import kotlinx.coroutines.flow.*
class BlockMiddleware(
private val middleware: Middleware,
private val events: EventProxy
private val middleware: Middleware
) : BlockRemote {
private val supportedEvents = listOf(
Events.Event.Message.ValueCase.BLOCKSHOW,
Events.Event.Message.ValueCase.BLOCKADD,
Events.Event.Message.ValueCase.BLOCKSETTEXT,
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS,
Events.Event.Message.ValueCase.BLOCKDELETE
)
private val supportedTextStyles = listOf(
Models.Block.Content.Text.Style.Paragraph,
Models.Block.Content.Text.Style.Header1,
Models.Block.Content.Text.Style.Header2,
Models.Block.Content.Text.Style.Header3,
Models.Block.Content.Text.Style.Title
)
private val supportedContent = listOf(
Models.Block.ContentCase.DASHBOARD,
Models.Block.ContentCase.PAGE,
Models.Block.ContentCase.LAYOUT
)
override suspend fun getConfig(): ConfigEntity {
return ConfigEntity(
homeId = middleware.provideHomeDashboardId()
)
}
override fun observeEvents(): Flow<EventEntity> = events
.flow()
.filter { event ->
event.messagesList.any { message ->
supportedEvents.contains(message.valueCase)
}
}
.map { event ->
event.messagesList.filter { message ->
supportedEvents.contains(message.valueCase)
}.map { message -> Pair(event.contextId, message) }
}
.flatMapConcat { event -> event.asFlow() }
.mapNotNull { (context, event) ->
when (event.valueCase) {
Events.Event.Message.ValueCase.BLOCKADD -> {
EventEntity.Command.AddBlock(
blocks = event.blockAdd.blocksList.mapNotNull { block ->
when (block.contentCase) {
Models.Block.ContentCase.DASHBOARD -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractDashboard(block)
)
}
Models.Block.ContentCase.PAGE -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractPage(block)
)
}
Models.Block.ContentCase.TEXT -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractText(block)
)
}
Models.Block.ContentCase.LAYOUT -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList,
fields = extractFields(block),
content = extractLayout(block)
)
}
Models.Block.ContentCase.LINK -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList,
fields = extractFields(block),
content = block.link()
)
}
else -> {
null
}
}
}
)
}
Events.Event.Message.ValueCase.BLOCKSHOW -> {
EventEntity.Command.ShowBlock(
rootId = event.blockShow.rootId,
blocks = event.blockShow.blocksList.mapNotNull { block ->
when (block.contentCase) {
Models.Block.ContentCase.DASHBOARD -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractDashboard(block)
)
}
Models.Block.ContentCase.PAGE -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractPage(block)
)
}
Models.Block.ContentCase.TEXT -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractText(block)
)
}
Models.Block.ContentCase.LAYOUT -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList,
fields = extractFields(block),
content = extractLayout(block)
)
}
Models.Block.ContentCase.LINK -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList,
fields = extractFields(block),
content = block.link()
)
}
else -> {
null
}
}
}
)
}
Events.Event.Message.ValueCase.BLOCKSETTEXT -> {
EventEntity.Command.UpdateBlockText(
id = event.blockSetText.id,
text = event.blockSetText.text.value
)
}
Events.Event.Message.ValueCase.BLOCKDELETE -> {
EventEntity.Command.DeleteBlock(
target = event.blockDelete.blockId
)
}
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> {
EventEntity.Command.UpdateStructure(
id = event.blockSetChildrenIds.id,
children = event.blockSetChildrenIds.childrenIdsList.toList(),
context = context
)
}
else -> null
}
}
override fun observeBlocks() = events
.flow()
.filter { event ->
event.messagesList.any { message ->
message.valueCase == Events.Event.Message.ValueCase.BLOCKSHOW
}
}
.map { event ->
event.messagesList.filter { message ->
message.valueCase == Events.Event.Message.ValueCase.BLOCKSHOW
}
}
.flatMapConcat { event -> event.asFlow() }
.map { event ->
event.blockShow.blocksList
.filter { block -> supportedContent.contains(block.contentCase) }
.map { block ->
when (block.contentCase) {
Models.Block.ContentCase.DASHBOARD -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractDashboard(block)
)
}
Models.Block.ContentCase.PAGE -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractPage(block)
)
}
Models.Block.ContentCase.TEXT -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractText(block)
)
}
Models.Block.ContentCase.LAYOUT -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList,
fields = extractFields(block),
content = extractLayout(block)
)
}
/*
Models.Block.ContentCase.IMAGE -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList,
fields = extractFields(block),
content = BlockEntity.Content.Image(
path = block.image.localFilePath
)
)
}
*/
else -> {
throw IllegalStateException("Unexpected content: ${block.contentCase}")
}
}
}
}
override fun observePages() = events
.flow()
.filter { event ->
event.messagesList.any { message ->
message.valueCase == Events.Event.Message.ValueCase.BLOCKSHOW
}
}
.map { event ->
event.messagesList.filter { message ->
message.valueCase == Events.Event.Message.ValueCase.BLOCKSHOW
}
}
.flatMapConcat { event -> event.asFlow() }
.map { event ->
event.blockShow.blocksList
.filter { block -> block.contentCase == Models.Block.ContentCase.TEXT }
.filter { block -> supportedTextStyles.contains(block.text.style) }
.map { block ->
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = extractFields(block),
content = extractText(block)
)
}
}
override suspend fun openDashboard(contextId: String, id: String) {
middleware.openDashboard(contextId, id)
}
@ -305,18 +34,6 @@ class BlockMiddleware(
middleware.closePage(id)
}
private fun extractFields(block: Models.Block): BlockEntity.Fields {
return BlockEntity.Fields().also { fields ->
block.fields.fieldsMap.mapValues { (key, value) ->
fields.map[key] = when (val case = value.kindCase) {
Value.KindCase.NUMBER_VALUE -> value.numberValue
Value.KindCase.STRING_VALUE -> value.stringValue
else -> throw IllegalStateException("$case is not supported.")
}
}
}
}
override suspend fun updateText(command: CommandEntity.UpdateText) {
middleware.updateText(
command.contextId,
@ -361,102 +78,4 @@ class BlockMiddleware(
override suspend fun unlink(command: CommandEntity.Unlink) {
middleware.unlink(command)
}
private fun extractDashboard(block: Models.Block): BlockEntity.Content.Dashboard {
return BlockEntity.Content.Dashboard(
type = when {
block.dashboard.style == Dashboard.Style.Archive -> {
BlockEntity.Content.Dashboard.Type.ARCHIVE
}
block.dashboard.style == Dashboard.Style.MainScreen -> {
BlockEntity.Content.Dashboard.Type.MAIN_SCREEN
}
else -> throw IllegalStateException("Unexpected dashboard style: ${block.dashboard.style}")
}
)
}
private fun extractPage(block: Models.Block): BlockEntity.Content.Page {
return BlockEntity.Content.Page(
style = when {
block.page.style == Page.Style.Empty -> {
BlockEntity.Content.Page.Style.EMPTY
}
block.page.style == Page.Style.Task -> {
BlockEntity.Content.Page.Style.TASK
}
block.page.style == Page.Style.Set -> {
BlockEntity.Content.Page.Style.SET
}
else -> throw IllegalStateException("Unexpected page style: ${block.page.style}")
}
)
}
private fun extractText(block: Models.Block): BlockEntity.Content.Text {
return BlockEntity.Content.Text(
text = block.text.text,
marks = block.text.marks.marksList.map { mark ->
BlockEntity.Content.Text.Mark(
range = IntRange(mark.range.from, mark.range.to),
param = if (mark.param.isNotEmpty()) mark.param else null,
type = when (mark.type) {
Models.Block.Content.Text.Mark.Type.Bold -> {
BlockEntity.Content.Text.Mark.Type.BOLD
}
Models.Block.Content.Text.Mark.Type.Italic -> {
BlockEntity.Content.Text.Mark.Type.ITALIC
}
Models.Block.Content.Text.Mark.Type.Strikethrough -> {
BlockEntity.Content.Text.Mark.Type.STRIKETHROUGH
}
Models.Block.Content.Text.Mark.Type.Underscored -> {
BlockEntity.Content.Text.Mark.Type.UNDERSCORED
}
Models.Block.Content.Text.Mark.Type.Keyboard -> {
BlockEntity.Content.Text.Mark.Type.KEYBOARD
}
Models.Block.Content.Text.Mark.Type.TextColor -> {
BlockEntity.Content.Text.Mark.Type.TEXT_COLOR
}
Models.Block.Content.Text.Mark.Type.BackgroundColor -> {
BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR
}
Models.Block.Content.Text.Mark.Type.Link -> {
BlockEntity.Content.Text.Mark.Type.LINK
}
else -> throw IllegalStateException("Unexpected mark type: ${mark.type.name}")
}
)
},
style = when (block.text.style) {
Models.Block.Content.Text.Style.Paragraph -> BlockEntity.Content.Text.Style.P
Models.Block.Content.Text.Style.Header1 -> BlockEntity.Content.Text.Style.H1
Models.Block.Content.Text.Style.Header2 -> BlockEntity.Content.Text.Style.H2
Models.Block.Content.Text.Style.Header3 -> BlockEntity.Content.Text.Style.H3
Models.Block.Content.Text.Style.Title -> BlockEntity.Content.Text.Style.TITLE
Models.Block.Content.Text.Style.Quote -> BlockEntity.Content.Text.Style.QUOTE
Models.Block.Content.Text.Style.Marked -> BlockEntity.Content.Text.Style.BULLET
Models.Block.Content.Text.Style.Numbered -> BlockEntity.Content.Text.Style.NUMBERED
Models.Block.Content.Text.Style.Toggle -> BlockEntity.Content.Text.Style.TOGGLE
Models.Block.Content.Text.Style.Checkbox -> BlockEntity.Content.Text.Style.CHECKBOX
else -> throw IllegalStateException("Unexpected text style: ${block.text.style}")
},
isChecked = block.text.checked
)
}
private fun extractLayout(block: Models.Block): BlockEntity.Content.Layout {
return BlockEntity.Content.Layout(
type = when {
block.layout.style == Models.Block.Content.Layout.Style.Column -> {
BlockEntity.Content.Layout.Type.COLUMN
}
block.layout.style == Models.Block.Content.Layout.Style.Row -> {
BlockEntity.Content.Layout.Type.ROW
}
else -> throw IllegalStateException("Unexpected layout style: ${block.layout.style}")
}
)
}
}

View file

@ -114,6 +114,8 @@ public class Middleware {
.setBlockId(id)
.build();
Timber.d("Opening home dashboard with the following request:\n%s", request.toString());
service.blockOpen(request);
}

View file

@ -6,6 +6,7 @@ import com.agileburo.anytype.data.auth.model.EventEntity
import com.agileburo.anytype.middleware.EventProxy
import com.agileburo.anytype.middleware.blocks
import com.agileburo.anytype.middleware.entity
import com.agileburo.anytype.middleware.fields
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
@ -23,61 +24,82 @@ class MiddlewareEventChannel(
Events.Event.Message.ValueCase.BLOCKADD,
Events.Event.Message.ValueCase.BLOCKSETTEXT,
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS,
Events.Event.Message.ValueCase.BLOCKDELETE
Events.Event.Message.ValueCase.BLOCKDELETE,
Events.Event.Message.ValueCase.BLOCKSETLINK
)
override fun observeEvents(): Flow<List<EventEntity>> = events
override fun observeEvents(
context: String?
): Flow<List<EventEntity>> = events
.flow()
.filter { context == null || it.contextId == context }
.map { event ->
event.messagesList.filter { message ->
supportedEvents.contains(message.valueCase)
}.map { message -> Pair(event.contextId, message) }
}
.filter { it.isNotEmpty() }
.map { events ->
events.mapNotNull { (context, event) ->
when (event.valueCase) {
Events.Event.Message.ValueCase.BLOCKADD -> {
EventEntity.Command.AddBlock(
blocks = event.blockAdd.blocksList.blocks()
)
}
Events.Event.Message.ValueCase.BLOCKSHOW -> {
EventEntity.Command.ShowBlock(
rootId = event.blockShow.rootId,
blocks = event.blockShow.blocksList.blocks()
)
}
Events.Event.Message.ValueCase.BLOCKSETTEXT -> {
EventEntity.Command.GranularChange(
id = event.blockSetText.id,
text = if (event.blockSetText.hasText())
event.blockSetText.text.value
else null,
style = if (event.blockSetText.hasStyle())
event.blockSetText.style.value.entity()
else
null,
color = if (event.blockSetText.hasColor())
event.blockSetText.color.value
else
null
)
}
Events.Event.Message.ValueCase.BLOCKDELETE -> {
EventEntity.Command.DeleteBlock(
target = event.blockDelete.blockId
)
}
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> {
EventEntity.Command.UpdateStructure(
id = event.blockSetChildrenIds.id,
children = event.blockSetChildrenIds.childrenIdsList.toList(),
context = context
)
}
else -> null
.map { events -> processEvents(events) }
private fun processEvents(events: List<Pair<String, Events.Event.Message>>): List<EventEntity.Command> {
return events.mapNotNull { (context, event) ->
when (event.valueCase) {
Events.Event.Message.ValueCase.BLOCKADD -> {
EventEntity.Command.AddBlock(
context = context,
blocks = event.blockAdd.blocksList.blocks()
)
}
Events.Event.Message.ValueCase.BLOCKSHOW -> {
EventEntity.Command.ShowBlock(
context = context,
rootId = event.blockShow.rootId,
blocks = event.blockShow.blocksList.blocks()
)
}
Events.Event.Message.ValueCase.BLOCKSETTEXT -> {
EventEntity.Command.GranularChange(
context = context,
id = event.blockSetText.id,
text = if (event.blockSetText.hasText())
event.blockSetText.text.value
else null,
style = if (event.blockSetText.hasStyle())
event.blockSetText.style.value.entity()
else
null,
color = if (event.blockSetText.hasColor())
event.blockSetText.color.value
else
null
)
}
Events.Event.Message.ValueCase.BLOCKDELETE -> {
EventEntity.Command.DeleteBlock(
context = context,
target = event.blockDelete.blockId
)
}
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> {
EventEntity.Command.UpdateStructure(
context = context,
id = event.blockSetChildrenIds.id,
children = event.blockSetChildrenIds.childrenIdsList.toList()
)
}
Events.Event.Message.ValueCase.BLOCKSETLINK -> {
EventEntity.Command.LinkGranularChange(
context = context,
id = event.blockSetLink.id,
target = event.blockSetLink.targetBlockId.value,
fields = if (event.blockSetLink.hasFields())
event.blockSetLink.fields.value.fields()
else
null
)
}
else -> null
}
}
}
}

View file

@ -0,0 +1,153 @@
package com.agileburo.anytype
import anytype.Events.Event
import anytype.Events.Event.Message
import com.agileburo.anytype.common.MockDataFactory
import com.agileburo.anytype.data.auth.model.EventEntity
import com.agileburo.anytype.middleware.EventProxy
import com.agileburo.anytype.middleware.interactor.MiddlewareEventChannel
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.stub
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
class MiddlewareEventChannelTest {
@Mock
lateinit var proxy: EventProxy
private lateinit var channel: MiddlewareEventChannel
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
channel = MiddlewareEventChannel(proxy)
}
@Test
fun `should filter event by context and pass it downstream`() {
val context = MockDataFactory.randomUuid()
val msg = Event.Block.Show
.newBuilder()
.setRootId(context)
.addAllBlocks(emptyList())
.build()
val message = Message
.newBuilder()
.setBlockShow(msg)
val event = Event
.newBuilder()
.setContextId(context)
.addMessages(message)
.build()
proxy.stub {
on { flow() } doReturn flowOf(event)
}
val expected = listOf(
EventEntity.Command.ShowBlock(
rootId = context,
blocks = emptyList(),
context = context
)
)
runBlocking {
channel.observeEvents(context = context).collect { events ->
assertEquals(
expected = expected,
actual = events
)
}
}
}
@Test
fun `should filter event by context and do not pass it downstream`() {
val context = MockDataFactory.randomUuid()
val msg = Event.Block.Show
.newBuilder()
.setRootId(MockDataFactory.randomString())
.addAllBlocks(emptyList())
.build()
val message = Message
.newBuilder()
.setBlockShow(msg)
val event = Event
.newBuilder()
.setContextId(MockDataFactory.randomUuid())
.addMessages(message)
.build()
proxy.stub {
on { flow() } doReturn flowOf(event)
}
runBlocking {
channel.observeEvents(context = context).collect { events ->
assertEquals(
expected = emptyList(),
actual = events
)
}
}
}
@Test
fun `should pass event downstream if context for filtering is not provided`() {
val context = MockDataFactory.randomUuid()
val msg = Event.Block.Show
.newBuilder()
.setRootId(context)
.addAllBlocks(emptyList())
.build()
val message = Message
.newBuilder()
.setBlockShow(msg)
val event = Event
.newBuilder()
.setContextId(context)
.addMessages(message)
.build()
proxy.stub {
on { flow() } doReturn flowOf(event)
}
val expected = listOf(
EventEntity.Command.ShowBlock(
rootId = context,
blocks = emptyList(),
context = context
)
)
runBlocking {
channel.observeEvents(context = context).collect { events ->
assertEquals(
expected = expected,
actual = events
)
}
}
}
}

View file

@ -0,0 +1,64 @@
package com.agileburo.anytype.common
import java.util.*
import java.util.concurrent.ThreadLocalRandom
object MockDataFactory {
fun randomUuid(): String {
return UUID.randomUUID().toString()
}
fun randomString(): String {
return randomUuid()
}
fun randomInt(): Int {
return ThreadLocalRandom.current().nextInt(0, 1000 + 1)
}
fun randomInt(max: Int): Int {
return ThreadLocalRandom.current().nextInt(0, max)
}
fun randomLong(): Long {
return randomInt().toLong()
}
fun randomFloat(): Float {
return randomInt().toFloat()
}
fun randomDouble(): Double {
return randomInt().toDouble()
}
fun randomBoolean(): Boolean {
return Math.random() < 0.5
}
fun makeIntList(count: Int): List<Int> {
val items = mutableListOf<Int>()
repeat(count) {
items.add(randomInt())
}
return items
}
fun makeStringList(count: Int): List<String> {
val items = mutableListOf<String>()
repeat(count) {
items.add(randomUuid())
}
return items
}
fun makeDoubleList(count: Int): List<Double> {
val items = mutableListOf<Double>()
repeat(count) {
items.add(randomDouble())
}
return items
}
}

View file

@ -0,0 +1,139 @@
package com.agileburo.anytype.presentation.desktop
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.dashboard.model.HomeDashboard
import com.agileburo.anytype.presentation.common.StateReducer
import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.launch
/**
* State machine for this view model consisting of [Interactor], [State], [Event] and [Reducer]
* It reduces [Event] to the immutable [State] by applying [Reducer] fuction.
* This [State] then will be rendered.
*/
sealed class HomeDashboardStateMachine {
class Interactor(
private val scope: CoroutineScope,
private val reducer: Reducer = Reducer(),
private val channel: Channel<Event> = Channel(),
private val events: Flow<Event> = channel.consumeAsFlow()
) {
fun onEvent(event: Event) = scope.launch { channel.send(event) }
fun state(): Flow<State> = events.scan(State.init(), reducer.function)
}
/**
* @property isInitialized whether this state is initialized
* @property isLoading whether the data is being loaded to prepare a new state
* @property error if present, represents an error occured in this state machine
* @property dashboard current dashboard data state that should be rendered
*/
data class State(
val isInitialzed: Boolean,
val isLoading: Boolean,
val error: String?,
val dashboard: HomeDashboard?
) : HomeDashboardStateMachine() {
companion object {
fun init() = State(
isInitialzed = true,
isLoading = false,
error = null,
dashboard = null
)
}
}
sealed class Event : HomeDashboardStateMachine() {
data class OnDashboardLoaded(
val dashboard: HomeDashboard
) : Event()
data class OnBlocksAdded(
val blocks: List<Block>
) : Event()
data class OnStructureUpdated(
val children: List<String>
) : Event()
data class OnLinkFieldsChanged(
val id: String,
val fields: Block.Fields
) : Event()
object OnDashboardLoadingStarted : Event()
object OnStartedCreatingPage : Event()
object OnFinishedCreatingPage : Event()
}
class Reducer : StateReducer<State, Event> {
override val function: suspend (State, Event) -> State
get() = { state, event -> reduce(state, event) }
override suspend fun reduce(
state: State, event: Event
) = when (event) {
is Event.OnDashboardLoadingStarted -> state.copy(
isInitialzed = true,
isLoading = true,
error = null,
dashboard = null
)
is Event.OnDashboardLoaded -> state.copy(
isInitialzed = true,
isLoading = false,
error = null,
dashboard = event.dashboard
)
is Event.OnStartedCreatingPage -> state.copy(
isLoading = true
)
is Event.OnFinishedCreatingPage -> state.copy(
isLoading = false
)
is Event.OnStructureUpdated -> state.copy(
isInitialzed = true,
isLoading = false,
dashboard = state.dashboard?.copy(
children = event.children
)
)
is Event.OnBlocksAdded -> state.copy(
isInitialzed = true,
isLoading = false,
dashboard = state.dashboard?.let { dashboard ->
dashboard.copy(blocks = dashboard.blocks + event.blocks)
}
)
is Event.OnLinkFieldsChanged -> state.copy(
dashboard = state.dashboard?.let { dashboard ->
dashboard.copy(
blocks = dashboard.blocks.map { block ->
if (block.id == event.id) {
val link = block.content.asLink()
block.copy(
content = link.copy(
fields = event.fields
)
)
} else {
block
}
}
)
}
)
}
}
}

View file

@ -10,27 +10,25 @@ import com.agileburo.anytype.domain.auth.interactor.GetCurrentAccount
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.block.interactor.DragAndDrop
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Position
import com.agileburo.anytype.domain.config.GetConfig
import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard
import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard
import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard
import com.agileburo.anytype.domain.dashboard.model.HomeDashboard
import com.agileburo.anytype.domain.event.interactor.ObserveEvents
import com.agileburo.anytype.domain.dashboard.interactor.toHomeDashboard
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.domain.page.CreatePage
import com.agileburo.anytype.presentation.common.StateReducer
import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel.Machine.*
import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.Interactor
import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.State
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.agileburo.anytype.presentation.navigation.SupportNavigation
import com.agileburo.anytype.presentation.profile.ProfileView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine as Machine
class HomeDashboardViewModel(
private val loadImage: LoadImage,
@ -38,10 +36,9 @@ class HomeDashboardViewModel(
private val openDashboard: OpenDashboard,
private val closeDashboard: CloseDashboard,
private val createPage: CreatePage,
private val observeHomeDashboard: ObserveHomeDashboard,
private val getConfig: GetConfig,
private val dragAndDrop: DragAndDrop,
private val observeEvents: ObserveEvents
private val interceptEvents: InterceptEvents
) : ViewStateViewModel<State>(),
SupportNavigation<EventWrapper<AppNavigation.Command>> {
@ -61,23 +58,55 @@ class HomeDashboardViewModel(
init {
startProcessingState()
startObservingEvents()
proceedWithGettingConfig()
}
private fun startProcessingState() {
viewModelScope.launch { machine.state().collect { stateData.postValue(it) } }
}
private fun startObservingEvents() {
observeEvents
.build()
.onEach { Timber.d("New event: $it") }
.onEach { event ->
if (event is Event.Command.UpdateStructure)
machine.onEvent(Machine.Event.OnStructureUpdated(event.children))
else if (event is Event.Command.AddBlock)
machine.onEvent(Machine.Event.OnBlocksAdded(event.blocks))
private fun startInterceptingEvents(context: String) {
// TODO use context when middleware is ready
interceptEvents
.build(InterceptEvents.Params(context = null))
.onEach { Timber.d("New events: $it") }
.onEach { events ->
events.forEach { event ->
when (event) {
is Event.Command.UpdateStructure -> machine.onEvent(
Machine.Event.OnStructureUpdated(
event.children
)
)
is Event.Command.AddBlock -> machine.onEvent(
Machine.Event.OnBlocksAdded(
event.blocks
)
)
is Event.Command.ShowBlock -> {
if (event.rootId == context) {
machine.onEvent(
Machine.Event.OnDashboardLoaded(
dashboard = event.blocks.toHomeDashboard(id = context)
)
)
} else {
Timber.e("Receiving event from other context!")
}
}
is Event.Command.LinkGranularChange -> {
event.fields?.let { fields ->
machine.onEvent(
Machine.Event.OnLinkFieldsChanged(
id = event.id,
fields = fields
)
)
}
}
}
}
}
.launchIn(viewModelScope)
}
@ -85,22 +114,15 @@ class HomeDashboardViewModel(
private fun proceedWithGettingConfig() {
getConfig.invoke(viewModelScope, Unit) { result ->
result.either(
fnR = {
startObservingHomeDashboard(id = it.homeDashboardId)
processDragAndDrop(context = it.homeDashboardId)
fnR = { config ->
startInterceptingEvents(context = config.home)
processDragAndDrop(context = config.home)
},
fnL = { Timber.e(it, "Error while getting config") }
)
}
}
private fun startObservingHomeDashboard(id: String) {
observeHomeDashboard
.build(ObserveHomeDashboard.Param(id))
.onEach { machine.onEvent(Machine.Event.OnDashboardLoaded(it)) }
.launchIn(viewModelScope)
}
private fun proceedWithGettingAccount() {
getCurrentAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
result.either(
@ -118,7 +140,7 @@ class HomeDashboardViewModel(
dropChanges
.withLatestFrom(movementChanges) { a, b -> Pair(a, b) }
.onEach { Timber.d("Dnd request: $it") }
.map { (subject, movement) ->
.mapLatest { (subject, movement) ->
DragAndDrop.Params(
context = context,
targetContext = context,
@ -140,7 +162,12 @@ class HomeDashboardViewModel(
private fun proceedWithOpeningHomeDashboard() {
machine.onEvent(Machine.Event.OnDashboardLoadingStarted)
openDashboard.invoke(viewModelScope, null) { result ->
Timber.d("Opening home dashboard")
// TODO replace params = null by more explicit code
openDashboard.invoke(
scope = viewModelScope,
params = null
) { result ->
result.either(
fnL = { Timber.e(it, "Error while opening home dashboard") },
fnR = { Timber.d("Home dashboard opened") }
@ -239,107 +266,4 @@ class HomeDashboardViewModel(
val target: String,
val direction: Position
)
/**
* State machine for this view model consisting of [Interactor], [State], [Event] and [Reducer]
* It reduces [Event] to the immutable [State] by applying [Reducer] fuction.
* This [State] then will be rendered.
*/
sealed class Machine {
class Interactor(
private val scope: CoroutineScope,
private val reducer: Reducer = Reducer(),
private val channel: Channel<Event> = Channel(),
private val events: Flow<Event> = channel.consumeAsFlow()
) {
fun onEvent(event: Event) = scope.launch { channel.send(event) }
fun state(): Flow<State> = events.scan(State.init(), reducer.function)
}
/**
* @property isInitialized whether this state is initialized
* @property isLoading whether the data is being loaded to prepare a new state
* @property error if present, represents an error occured in this state machine
* @property homeDashboard current dashboard data state that should be rendered
*/
data class State(
val isInitialzed: Boolean,
val isLoading: Boolean,
val error: String?,
val homeDashboard: HomeDashboard?
) : Machine() {
companion object {
fun init() = State(
isInitialzed = true,
isLoading = false,
error = null,
homeDashboard = null
)
}
}
sealed class Event : Machine() {
data class OnDashboardLoaded(
val dashboard: HomeDashboard
) : Event()
data class OnBlocksAdded(
val blocks: List<Block>
) : Event()
data class OnStructureUpdated(
val children: List<String>
) : Event()
object OnDashboardLoadingStarted : Event()
object OnStartedCreatingPage : Event()
object OnFinishedCreatingPage : Event()
}
class Reducer : StateReducer<State, Event> {
override val function: suspend (State, Event) -> State
get() = { state, event -> reduce(state, event) }
override suspend fun reduce(
state: State, event: Event
) = when (event) {
is Event.OnDashboardLoadingStarted -> state.copy(
isInitialzed = true,
isLoading = true,
error = null,
homeDashboard = null
)
is Event.OnDashboardLoaded -> state.copy(
isInitialzed = true,
isLoading = false,
error = null,
homeDashboard = event.dashboard
)
is Event.OnStartedCreatingPage -> state.copy(
isLoading = true
)
is Event.OnFinishedCreatingPage -> state.copy(
isLoading = false
)
is Event.OnStructureUpdated -> state.copy(
isInitialzed = true,
isLoading = false,
homeDashboard = state.homeDashboard?.copy(
children = event.children
)
)
is Event.OnBlocksAdded -> state.copy(
isInitialzed = true,
isLoading = false,
homeDashboard = state.homeDashboard?.let { dashboard ->
dashboard.copy(blocks = dashboard.blocks + event.blocks)
}
)
}
}
}
}

View file

@ -6,9 +6,8 @@ import com.agileburo.anytype.domain.auth.interactor.GetCurrentAccount
import com.agileburo.anytype.domain.block.interactor.DragAndDrop
import com.agileburo.anytype.domain.config.GetConfig
import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard
import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard
import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard
import com.agileburo.anytype.domain.event.interactor.ObserveEvents
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.domain.page.CreatePage
@ -19,9 +18,8 @@ class HomeDashboardViewModelFactory(
private val closeDashboard: CloseDashboard,
private val createPage: CreatePage,
private val getConfig: GetConfig,
private val observeHomeDashboard: ObserveHomeDashboard,
private val dnd: DragAndDrop,
private val observeEvents: ObserveEvents
private val interceptEvents: InterceptEvents
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -33,9 +31,8 @@ class HomeDashboardViewModelFactory(
closeDashboard = closeDashboard,
createPage = createPage,
getConfig = getConfig,
observeHomeDashboard = observeHomeDashboard,
dragAndDrop = dnd,
observeEvents = observeEvents
interceptEvents = interceptEvents
) as T
}
}

View file

@ -1,2 +0,0 @@
package com.agileburo.anytype.presentation.desktop

View file

@ -347,6 +347,7 @@ class PageViewModel(
}
fun open(id: String) {
pageId = id
stateData.postValue(ViewState.Loading)

View file

@ -13,15 +13,15 @@ import com.agileburo.anytype.domain.block.model.Position
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.config.GetConfig
import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard
import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard
import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard
import com.agileburo.anytype.domain.dashboard.interactor.toHomeDashboard
import com.agileburo.anytype.domain.dashboard.model.HomeDashboard
import com.agileburo.anytype.domain.event.interactor.ObserveEvents
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.domain.page.CreatePage
import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine
import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel
import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel.Machine.State
import com.agileburo.anytype.presentation.mapper.toView
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.agileburo.anytype.presentation.profile.ProfileView
@ -56,9 +56,6 @@ class HomeDashboardViewModelTest {
@Mock
lateinit var openDashboard: OpenDashboard
@Mock
lateinit var observeHomeDashboard: ObserveHomeDashboard
@Mock
lateinit var getConfig: GetConfig
@ -69,7 +66,7 @@ class HomeDashboardViewModelTest {
lateinit var createPage: CreatePage
@Mock
lateinit var observeEvents: ObserveEvents
lateinit var interceptEvents: InterceptEvents
@Mock
lateinit var dnd: DragAndDrop
@ -88,23 +85,20 @@ class HomeDashboardViewModelTest {
openDashboard = openDashboard,
closeDashboard = closeDashboard,
createPage = createPage,
observeHomeDashboard = observeHomeDashboard,
getConfig = getConfig,
dragAndDrop = dnd,
observeEvents = observeEvents
interceptEvents = interceptEvents
)
}
@Test
fun `should only start getting config when view model is initialized`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val param = ObserveHomeDashboard.Param(config.homeDashboardId)
val config = Config(home = MockDataFactory.randomUuid())
val response = Either.Right(config)
stubGetConfig(response)
stubObserveHomeDashboard(param)
stubObserveEvents()
stubObserveEvents(params = InterceptEvents.Params(context = null))
vm = buildViewModel()
@ -115,35 +109,34 @@ class HomeDashboardViewModelTest {
}
@Test
fun `should start observing events when view model is initialized`() {
fun `should start observing events after receiving config`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val config = Config(home = MockDataFactory.randomUuid())
val response = Either.Right(config)
val params = InterceptEvents.Params(context = null)
stubGetConfig(response)
stubObserveHomeDashboard()
stubObserveEvents()
stubObserveEvents(params = params)
vm = buildViewModel()
verify(observeEvents, times(1)).build()
verify(getConfig, times(1)).invoke(any(), any(), any())
verify(interceptEvents, times(1)).build(params)
}
@Test
fun `should start observing home dashboard after receiving config`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val config = Config(home = MockDataFactory.randomUuid())
val response = Either.Right(config)
val param = ObserveHomeDashboard.Param(id = config.homeDashboardId)
stubGetConfig(response)
stubObserveHomeDashboard()
stubObserveEvents()
stubObserveEvents(params = InterceptEvents.Params(context = null))
vm = buildViewModel()
verify(getConfig, times(1)).invoke(any(), any(), any())
verify(observeHomeDashboard, times(1)).build(param)
verifyZeroInteractions(openDashboard)
verifyZeroInteractions(loadImage)
verifyZeroInteractions(getCurrentAccount)
@ -152,20 +145,19 @@ class HomeDashboardViewModelTest {
@Test
fun `should emit loading state when home dashboard loading started`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val config = Config(home = MockDataFactory.randomUuid())
stubGetConfig(Either.Right(config))
stubObserveHomeDashboard()
stubObserveEvents()
stubObserveEvents(params = InterceptEvents.Params(context = null))
vm = buildViewModel()
vm.onViewCreated()
val expected = State(
val expected = HomeDashboardStateMachine.State(
isLoading = true,
isInitialzed = true,
homeDashboard = null,
dashboard = null,
error = null
)
@ -175,7 +167,7 @@ class HomeDashboardViewModelTest {
@Test
fun `should emit view state with dashboard when home dashboard loading started`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val config = Config(home = MockDataFactory.randomUuid())
val page = Block(
id = MockDataFactory.randomUuid(),
@ -186,27 +178,35 @@ class HomeDashboardViewModelTest {
)
)
val dashboard = HomeDashboard(
id = config.homeDashboardId,
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
blocks = listOf(page),
children = listOf(page.id)
val dashboard = Block(
id = config.home,
content = Block.Content.Dashboard(
type = Block.Content.Dashboard.Type.MAIN_SCREEN
),
children = listOf(page.id),
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString()))
)
val delayInMillis = 100L
val flow = flow {
val events = flow {
delay(delayInMillis)
emit(dashboard)
emit(
listOf(
Event.Command.ShowBlock(
rootId = config.home,
context = config.home,
blocks = listOf(dashboard, page)
)
)
)
}
stubGetConfig(Either.Right(config))
stubObserveHomeDashboard(
flow = flow,
param = ObserveHomeDashboard.Param(id = dashboard.id)
stubObserveEvents(
params = InterceptEvents.Params(context = null),
flow = events
)
stubObserveEvents()
stubOpenDashboard()
vm = buildViewModel()
@ -214,10 +214,10 @@ class HomeDashboardViewModelTest {
vm.onViewCreated()
vm.state.test().assertValue(
State(
HomeDashboardStateMachine.State(
isLoading = true,
isInitialzed = true,
homeDashboard = null,
dashboard = null,
error = null
)
)
@ -225,10 +225,10 @@ class HomeDashboardViewModelTest {
coroutineTestRule.advanceTime(delayInMillis)
vm.state.test().assertValue(
State(
HomeDashboardStateMachine.State(
isLoading = false,
isInitialzed = true,
homeDashboard = dashboard,
dashboard = listOf(dashboard, page).toHomeDashboard(dashboard.id),
error = null
)
)
@ -237,48 +237,62 @@ class HomeDashboardViewModelTest {
@Test
fun `block dragging events do not alter overall state`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val config = Config(home = MockDataFactory.randomUuid())
val pages = listOf(
Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Link(
target = MockDataFactory.randomUuid(),
type = Block.Content.Link.Type.PAGE,
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
isArchived = MockDataFactory.randomBoolean()
)
),
Block(
id = MockDataFactory.randomUuid(),
children = emptyList(),
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
content = Block.Content.Link(
target = MockDataFactory.randomUuid(),
type = Block.Content.Link.Type.PAGE,
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
isArchived = MockDataFactory.randomBoolean()
)
)
)
val dashboard = HomeDashboard(
id = config.homeDashboardId,
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
blocks = pages,
children = pages.map { it.id }
val dashboard = Block(
id = config.home,
content = Block.Content.Dashboard(
type = Block.Content.Dashboard.Type.MAIN_SCREEN
),
children = pages.map { page -> page.id },
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString()))
)
val delayInMillis = 100L
val flow = flow {
val events = flow {
delay(delayInMillis)
emit(dashboard)
emit(
listOf(
Event.Command.ShowBlock(
rootId = config.home,
context = config.home,
blocks = listOf(dashboard) + pages
)
)
)
}
stubGetConfig(Either.Right(config))
stubObserveHomeDashboard(
flow = flow,
param = ObserveHomeDashboard.Param(id = dashboard.id)
stubObserveEvents(
params = InterceptEvents.Params(context = null),
flow = events
)
stubObserveEvents()
stubOpenDashboard()
vm = buildViewModel()
@ -287,14 +301,19 @@ class HomeDashboardViewModelTest {
coroutineTestRule.advanceTime(delayInMillis)
val expected = State(
val expected = HomeDashboardStateMachine.State(
isLoading = false,
isInitialzed = true,
homeDashboard = dashboard,
dashboard = listOf(
dashboard,
pages.first(),
pages.last()
).toHomeDashboard(dashboard.id),
error = null
)
val views = dashboard.toView()
val views =
listOf(dashboard, pages.first(), pages.last()).toHomeDashboard(dashboard.id).toView()
val from = 0
val to = 1
@ -314,7 +333,7 @@ class HomeDashboardViewModelTest {
@Test
fun `should start dispatching drag-and-drop actions when the dragged item is dropped`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val config = Config(home = MockDataFactory.randomUuid())
val pages = listOf(
Block(
@ -342,7 +361,7 @@ class HomeDashboardViewModelTest {
)
val dashboard = HomeDashboard(
id = config.homeDashboardId,
id = config.home,
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
blocks = pages,
@ -357,11 +376,7 @@ class HomeDashboardViewModelTest {
}
stubGetConfig(Either.Right(config))
stubObserveHomeDashboard(
flow = flow,
param = ObserveHomeDashboard.Param(id = dashboard.id)
)
stubObserveEvents()
stubObserveEvents(params = InterceptEvents.Params(context = null))
stubOpenDashboard()
vm = buildViewModel()
@ -387,8 +402,8 @@ class HomeDashboardViewModelTest {
scope = any(),
params = eq(
DragAndDrop.Params(
context = config.homeDashboardId,
targetContext = config.homeDashboardId,
context = config.home,
targetContext = config.home,
targetId = pages.last().content.asLink().target,
blockIds = listOf(pages.first().content.asLink().target),
position = Position.BOTTOM
@ -401,10 +416,9 @@ class HomeDashboardViewModelTest {
@Test
fun `should proceed with getting account and opening dashboard when view is created`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val config = Config(home = MockDataFactory.randomUuid())
stubGetConfig(Either.Right(config))
stubObserveHomeDashboard()
stubObserveEvents()
stubObserveEvents(params = InterceptEvents.Params(context = null))
vm = buildViewModel()
vm.onViewCreated()
@ -425,8 +439,6 @@ class HomeDashboardViewModelTest {
val response = Either.Right(account)
stubObserveHomeDashboard()
stubObserveEvents()
stubGetCurrentAccount(response)
vm = buildViewModel()
@ -456,7 +468,6 @@ class HomeDashboardViewModelTest {
val accountResponse = Either.Right(account)
val imageResponse = Either.Right(blob)
stubObserveHomeDashboard()
stubObserveEvents()
stubGetCurrentAccount(accountResponse)
@ -491,7 +502,6 @@ class HomeDashboardViewModelTest {
val accountResponse = Either.Right(account)
stubObserveHomeDashboard()
stubObserveEvents()
stubGetCurrentAccount(accountResponse)
@ -506,7 +516,6 @@ class HomeDashboardViewModelTest {
@Test
fun `should start creating page when requested from UI`() {
stubObserveHomeDashboard()
stubObserveEvents()
vm = buildViewModel()
@ -521,7 +530,6 @@ class HomeDashboardViewModelTest {
val id = MockDataFactory.randomUuid()
stubObserveHomeDashboard()
stubObserveEvents()
closeDashboard.stub {
@ -549,60 +557,64 @@ class HomeDashboardViewModelTest {
}
@Test
fun `should update state when a new block is added`() {
fun `should update state when a new block is added without updating dashboard children structure`() {
val config = Config(homeDashboardId = MockDataFactory.randomUuid())
val config = Config(home = "HOME_ID")
val page = Block(
id = MockDataFactory.randomUuid(),
id = "FIRST_PAGE_ID",
children = emptyList(),
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
fields = Block.Fields(map = mapOf("name" to "FIRST_PAGE")),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
)
)
val new = Block(
id = MockDataFactory.randomUuid(),
id = "NEW_BLOCK_ID",
children = emptyList(),
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
fields = Block.Fields(map = mapOf("name" to "SECOND_PAGE")),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
)
)
val dashboard = HomeDashboard(
id = config.homeDashboardId,
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
blocks = listOf(page),
children = listOf(page.id)
val dashboardName = "HOME"
val dashboard = Block(
id = config.home,
content = Block.Content.Dashboard(
type = Block.Content.Dashboard.Type.MAIN_SCREEN
),
children = listOf(page.id),
fields = Block.Fields(map = mapOf("name" to dashboardName))
)
val event = Event.Command.AddBlock(
blocks = listOf(new)
val showDashboardEvent = Event.Command.ShowBlock(
rootId = config.home,
context = config.home,
blocks = listOf(dashboard, page)
)
val addBlockEvent = Event.Command.AddBlock(
blocks = listOf(new),
context = config.home
)
val dashboardDelayInMillis = 100L
val eventDelayInMillis = 200L
val dashboardFlow = flow {
val events: Flow<List<Event>> = flow {
delay(dashboardDelayInMillis)
emit(dashboard)
}
val eventFlow = flow {
emit(listOf(showDashboardEvent))
delay(eventDelayInMillis)
emit(event)
emit(listOf(addBlockEvent))
}
stubGetConfig(Either.Right(config))
stubObserveHomeDashboard(
flow = dashboardFlow,
param = ObserveHomeDashboard.Param(id = dashboard.id)
)
stubObserveEvents(
flow = eventFlow
flow = events,
params = InterceptEvents.Params(context = null)
)
stubOpenDashboard()
@ -612,11 +624,27 @@ class HomeDashboardViewModelTest {
coroutineTestRule.advanceTime(dashboardDelayInMillis)
val firstExpectedState = HomeDashboard(
id = config.home,
fields = Block.Fields(map = mapOf("name" to dashboardName)),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
blocks = listOf(page),
children = listOf(page.id)
)
val secondExpectedState = HomeDashboard(
id = config.home,
fields = Block.Fields(map = mapOf("name" to dashboardName)),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
blocks = listOf(page, new),
children = listOf(page.id)
)
vm.state.test().assertValue(
State(
HomeDashboardStateMachine.State(
isLoading = false,
isInitialzed = true,
homeDashboard = dashboard,
dashboard = firstExpectedState,
error = null
)
)
@ -624,26 +652,15 @@ class HomeDashboardViewModelTest {
coroutineTestRule.advanceTime(eventDelayInMillis)
vm.state.test().assertValue(
State(
HomeDashboardStateMachine.State(
isLoading = false,
isInitialzed = true,
homeDashboard = dashboard.copy(
blocks = dashboard.blocks + listOf(new)
),
dashboard = secondExpectedState,
error = null
)
)
}
private fun stubObserveHomeDashboard(
param: ObserveHomeDashboard.Param = any(),
flow: Flow<HomeDashboard> = flowOf()
) {
observeHomeDashboard.stub {
onBlocking { build(param) } doReturn flow
}
}
private fun stubGetConfig(response: Either.Right<Config>) {
getConfig.stub {
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
@ -652,9 +669,12 @@ class HomeDashboardViewModelTest {
}
}
private fun stubObserveEvents(flow: Flow<Event> = flowOf()) {
observeEvents.stub {
onBlocking { build() } doReturn flow
private fun stubObserveEvents(
flow: Flow<List<Event>> = flowOf(),
params: InterceptEvents.Params? = null
) {
interceptEvents.stub {
onBlocking { build(params) } doReturn flow
}
}

View file

@ -144,7 +144,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -318,7 +319,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -335,7 +337,8 @@ class PageViewModelTest {
emit(
listOf(
Event.Command.AddBlock(
blocks = listOf(added)
blocks = listOf(added),
context = root
)
)
)
@ -396,7 +399,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -405,7 +409,8 @@ class PageViewModelTest {
listOf(
Event.Command.UpdateBlockText(
text = text,
id = child
id = child,
context = root
)
)
)
@ -488,7 +493,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = blocks
blocks = blocks,
context = root
)
)
)
@ -617,7 +623,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = blocks
blocks = blocks,
context = root
)
)
)
@ -750,7 +757,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = blocks
blocks = blocks,
context = root
)
)
)
@ -833,7 +841,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = blocks
blocks = blocks,
context = root
)
)
)
@ -924,7 +933,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = blocks
blocks = blocks,
context = root
)
)
)
@ -1007,7 +1017,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = blocks
blocks = blocks,
context = root
)
)
)
@ -1068,7 +1079,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1130,7 +1142,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1193,7 +1206,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1262,7 +1276,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1328,7 +1343,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1412,7 +1428,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1429,7 +1446,8 @@ class PageViewModelTest {
emit(
listOf(
Event.Command.AddBlock(
blocks = listOf(new)
blocks = listOf(new),
context = root
)
)
)
@ -1482,7 +1500,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1524,7 +1543,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1566,7 +1586,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1616,7 +1637,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1624,7 +1646,8 @@ class PageViewModelTest {
emit(
listOf(
Event.Command.DeleteBlock(
target = firstChild
target = firstChild,
context = root
)
)
)
@ -1680,7 +1703,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1725,7 +1749,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1765,7 +1790,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1812,7 +1838,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1864,7 +1891,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1907,7 +1935,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -1960,7 +1989,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -2006,7 +2036,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -2061,7 +2092,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -2117,7 +2149,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -2173,7 +2206,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)
@ -2222,7 +2256,8 @@ class PageViewModelTest {
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page
blocks = page,
context = root
)
)
)