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:
parent
7ed1181533
commit
48159c24af
39 changed files with 829 additions and 1080 deletions
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() } }
|
||||
}
|
|
@ -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>>
|
||||
}
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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>>
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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>>
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>>
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
package com.agileburo.anytype.presentation.desktop
|
||||
|
|
@ -347,6 +347,7 @@ class PageViewModel(
|
|||
}
|
||||
|
||||
fun open(id: String) {
|
||||
|
||||
pageId = id
|
||||
|
||||
stateData.postValue(ViewState.Loading)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue