diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 54ac043ffc..54f78d1407 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,7 +1,7 @@ on: pull_request: # add "synchronize" in "types", in order to trigger workflow for pull request commit(s) pushes. - types: [open,synchronize] + types: [synchronize] branches: [develop] name: Run debug unit tests jobs: diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/SlashActionsAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/SlashActionsAdapter.kt new file mode 100644 index 0000000000..3c86f41aab --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/SlashActionsAdapter.kt @@ -0,0 +1,76 @@ +package com.anytypeio.anytype.core_ui.features.page.slash + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.features.page.slash.holders.ActionMenuHolder +import com.anytypeio.anytype.core_ui.features.page.slash.holders.SubheaderMenuHolder +import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem +import kotlinx.android.synthetic.main.item_slash_widget_subheader.view.* + +class SlashActionsAdapter( + private var items: List, + private val clicks: (SlashItem) -> Unit, + private val clickBack: () -> Unit +) : RecyclerView.Adapter() { + + fun update(items: List) { + this.items = items + notifyDataSetChanged() + } + + fun clear() { + val size = items.size + if (size > 0) { + items = listOf() + notifyItemRangeRemoved(0, size) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + R.layout.item_slash_widget_style -> { + ActionMenuHolder( + view = inflater.inflate(viewType, parent, false) + ).apply { + itemView.setOnClickListener { + clicks(items[bindingAdapterPosition]) + } + } + } + R.layout.item_slash_widget_subheader -> { + SubheaderMenuHolder( + view = inflater.inflate(viewType, parent, false) + ).apply { + itemView.flBack.setOnClickListener { + clickBack.invoke() + } + } + } + else -> throw IllegalArgumentException("Wrong viewtype:$viewType") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is ActionMenuHolder -> { + val item = items[position] as SlashItem.Actions + holder.bind(item) + } + is SubheaderMenuHolder -> { + val item = items[position] as SlashItem.Subheader + holder.bind(item) + } + } + } + + override fun getItemViewType(position: Int): Int = when (val item = items[position]) { + is SlashItem.Actions -> R.layout.item_slash_widget_style + is SlashItem.Subheader -> R.layout.item_slash_widget_subheader + else -> throw IllegalArgumentException("Wrong item type:$item") + } + + override fun getItemCount(): Int = items.size +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/SlashWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/SlashWidget.kt index 6db7367e24..5c9d158b25 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/SlashWidget.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/SlashWidget.kt @@ -7,6 +7,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.features.page.slash.holders.SlashAlignmentAdapter import com.anytypeio.anytype.core_ui.tools.SlashHelper import com.anytypeio.anytype.presentation.page.editor.slash.SlashCommand import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem @@ -73,13 +74,31 @@ class SlashWidget @JvmOverloads constructor( ) } + private val actionsAdapter by lazy { + SlashActionsAdapter( + items = listOf(), + clicks = { _clickEvents.offer(it) }, + clickBack = { _backEvent.offer(true) } + ) + } + + private val alignAdapter by lazy { + SlashAlignmentAdapter( + items = listOf(), + clicks = { _clickEvents.offer(it) }, + clickBack = { _backEvent.offer(true) } + ) + } + private val concatAdapter = ConcatAdapter( mainAdapter, styleAdapter, mediaAdapter, objectTypesAdapter, relationsAdapter, - otherAdapter + otherAdapter, + actionsAdapter, + alignAdapter ) init { @@ -106,6 +125,8 @@ class SlashWidget @JvmOverloads constructor( objectTypesAdapter.clear() relationsAdapter.clear() otherAdapter.clear() + actionsAdapter.clear() + alignAdapter.clear() } is SlashCommand.ShowStyleItems -> { styleAdapter.update(command.items) @@ -116,6 +137,8 @@ class SlashWidget @JvmOverloads constructor( objectTypesAdapter.clear() relationsAdapter.clear() otherAdapter.clear() + actionsAdapter.clear() + alignAdapter.clear() } is SlashCommand.ShowMediaItems -> { mediaAdapter.update(command.items) @@ -126,6 +149,8 @@ class SlashWidget @JvmOverloads constructor( objectTypesAdapter.clear() relationsAdapter.clear() otherAdapter.clear() + actionsAdapter.clear() + alignAdapter.clear() } is SlashCommand.ShowRelations -> { relationsAdapter.update(command.relations) @@ -136,6 +161,8 @@ class SlashWidget @JvmOverloads constructor( mediaAdapter.clear() objectTypesAdapter.clear() otherAdapter.clear() + actionsAdapter.clear() + alignAdapter.clear() } is SlashCommand.ShowObjectTypes -> { objectTypesAdapter.update(command.items) @@ -146,15 +173,44 @@ class SlashWidget @JvmOverloads constructor( mediaAdapter.clear() relationsAdapter.clear() otherAdapter.clear() + actionsAdapter.clear() + alignAdapter.clear() } is SlashCommand.ShowOtherItems -> { otherAdapter.update(command.items) + rvSlash.smoothScrollToPosition(0) mainAdapter.clear() styleAdapter.clear() mediaAdapter.clear() objectTypesAdapter.clear() relationsAdapter.clear() + actionsAdapter.clear() + alignAdapter.clear() + } + is SlashCommand.ShowActionItems -> { + actionsAdapter.update(command.items) + rvSlash.smoothScrollToPosition(0) + + mainAdapter.clear() + styleAdapter.clear() + mediaAdapter.clear() + objectTypesAdapter.clear() + relationsAdapter.clear() + otherAdapter.clear() + alignAdapter.clear() + } + is SlashCommand.ShowAlignmentItems -> { + alignAdapter.update(command.items) + rvSlash.smoothScrollToPosition(0) + + mainAdapter.clear() + styleAdapter.clear() + mediaAdapter.clear() + objectTypesAdapter.clear() + relationsAdapter.clear() + otherAdapter.clear() + actionsAdapter.clear() } is SlashCommand.FilterItems -> { val filter = command.filter.removePrefix(SLASH_PREFIX) diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/ActionMenuHolder.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/ActionMenuHolder.kt new file mode 100644 index 0000000000..5af4980964 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/ActionMenuHolder.kt @@ -0,0 +1,51 @@ +package com.anytypeio.anytype.core_ui.features.page.slash.holders + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_utils.ext.gone +import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem +import kotlinx.android.synthetic.main.item_slash_widget_style.view.* + +class ActionMenuHolder(view: View) : RecyclerView.ViewHolder(view) { + + fun bind(item: SlashItem.Actions) = with(itemView) { + when (item) { + SlashItem.Actions.CleanStyle -> { + tvTitle.setText(R.string.slash_widget_actions_clean_style) + ivIcon.setImageResource(R.drawable.ic_slash_actions_clean_style) + tvSubtitle.gone() + } + SlashItem.Actions.Copy -> { + tvTitle.setText(R.string.slash_widget_actions_copy) + ivIcon.setImageResource(R.drawable.ic_slash_actions_copy) + tvSubtitle.gone() + } + SlashItem.Actions.Delete -> { + tvTitle.setText(R.string.slash_widget_actions_delete) + ivIcon.setImageResource(R.drawable.ic_slash_actions_delete) + tvSubtitle.gone() + } + SlashItem.Actions.Duplicate -> { + tvTitle.setText(R.string.slash_widget_actions_duplicate) + ivIcon.setImageResource(R.drawable.ic_slash_actions_duplicate) + tvSubtitle.gone() + } + SlashItem.Actions.Move -> { + tvTitle.setText(R.string.slash_widget_actions_move) + ivIcon.setImageResource(R.drawable.ic_slash_actions_move) + tvSubtitle.gone() + } + SlashItem.Actions.MoveTo -> { + tvTitle.setText(R.string.slash_widget_actions_moveto) + ivIcon.setImageResource(R.drawable.ic_slash_actions_move_to) + tvSubtitle.gone() + } + SlashItem.Actions.Paste -> { + tvTitle.setText(R.string.slash_widget_actions_paste) + ivIcon.setImageResource(R.drawable.ic_slash_actions_clean_style) + tvSubtitle.gone() + } + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/AlignMenuHolder.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/AlignMenuHolder.kt new file mode 100644 index 0000000000..cb1229c44e --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/AlignMenuHolder.kt @@ -0,0 +1,31 @@ +package com.anytypeio.anytype.core_ui.features.page.slash.holders + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_utils.ext.gone +import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem +import kotlinx.android.synthetic.main.item_slash_widget_style.view.* + +class AlignMenuHolder(view: View) : RecyclerView.ViewHolder(view) { + + fun bind(item: SlashItem.Alignment) = with(itemView) { + when (item) { + SlashItem.Alignment.Left -> { + tvTitle.setText(R.string.slash_widget_align_left) + ivIcon.setImageResource(R.drawable.ic_slash_align_left) + tvSubtitle.gone() + } + SlashItem.Alignment.Center -> { + tvTitle.setText(R.string.slash_widget_align_center) + ivIcon.setImageResource(R.drawable.ic_slash_align_center) + tvSubtitle.gone() + } + SlashItem.Alignment.Right -> { + tvTitle.setText(R.string.slash_widget_align_right) + ivIcon.setImageResource(R.drawable.ic_slash_align_right) + tvSubtitle.gone() + } + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/SlashAlignmentAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/SlashAlignmentAdapter.kt new file mode 100644 index 0000000000..75c8ef596e --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/SlashAlignmentAdapter.kt @@ -0,0 +1,74 @@ +package com.anytypeio.anytype.core_ui.features.page.slash.holders + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem +import kotlinx.android.synthetic.main.item_slash_widget_subheader.view.* + +class SlashAlignmentAdapter( + private var items: List, + private val clicks: (SlashItem) -> Unit, + private val clickBack: () -> Unit +) : RecyclerView.Adapter() { + + fun update(items: List) { + this.items = items + notifyDataSetChanged() + } + + fun clear() { + val size = items.size + if (size > 0) { + items = listOf() + notifyItemRangeRemoved(0, size) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + R.layout.item_slash_widget_style -> { + AlignMenuHolder( + view = inflater.inflate(viewType, parent, false) + ).apply { + itemView.setOnClickListener { + clicks(items[bindingAdapterPosition]) + } + } + } + R.layout.item_slash_widget_subheader -> { + SubheaderMenuHolder( + view = inflater.inflate(viewType, parent, false) + ).apply { + itemView.flBack.setOnClickListener { + clickBack.invoke() + } + } + } + else -> throw IllegalArgumentException("Wrong viewtype:$viewType") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is AlignMenuHolder -> { + val item = items[position] as SlashItem.Alignment + holder.bind(item) + } + is SubheaderMenuHolder -> { + val item = items[position] as SlashItem.Subheader + holder.bind(item) + } + } + } + + override fun getItemViewType(position: Int): Int = when (val item = items[position]) { + is SlashItem.Alignment -> R.layout.item_slash_widget_style + is SlashItem.Subheader -> R.layout.item_slash_widget_subheader + else -> throw IllegalArgumentException("Wrong item type:$item") + } + + override fun getItemCount(): Int = items.size +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/SubheaderMenuHolder.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/SubheaderMenuHolder.kt index aae641977d..97e69e3e53 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/SubheaderMenuHolder.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/page/slash/holders/SubheaderMenuHolder.kt @@ -39,6 +39,38 @@ class SubheaderMenuHolder(view: View) : RecyclerView.ViewHolder(view) { flBack.visible() R.string.slash_widget_main_other } + SlashItem.Subheader.Actions -> { + flBack.invisible() + R.string.slash_widget_main_actions + } + SlashItem.Subheader.ActionsWithBack -> { + flBack.visible() + R.string.slash_widget_main_actions + } + SlashItem.Subheader.Alignment -> { + flBack.invisible() + R.string.slash_widget_main_alignment + } + SlashItem.Subheader.AlignmentWithBack -> { + flBack.visible() + R.string.slash_widget_main_alignment + } + SlashItem.Subheader.Color -> { + flBack.invisible() + R.string.slash_widget_main_color + } + SlashItem.Subheader.ColorWithBack -> { + flBack.visible() + R.string.slash_widget_main_color + } + SlashItem.Subheader.Background -> { + flBack.invisible() + R.string.slash_widget_main_background + } + SlashItem.Subheader.BackgroundWithBack -> { + flBack.visible() + R.string.slash_widget_main_background + } } subheader.setText(text) } diff --git a/dependencies.gradle b/dependencies.gradle index 78cd9e1eca..c3364a4b8a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -54,6 +54,7 @@ ext { robolectric_latest_version = '4.5.1' junit_version = '4.12' kluent_version = '1.14' + timber_junit = '1.0.1' coroutine_testing_version = '1.4.3' live_data_testing_version = '1.1.0' mockito_kotlin_version = '3.2.0' @@ -147,7 +148,8 @@ ext { liveDataTesting: "com.jraska.livedata:testing-ktx:$live_data_testing_version", coroutineTesting: "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_testing_version", assertj: "com.squareup.assertj:assertj-android:1.0.0", - androidXTestCore: "androidx.test:core:$androidx_test_core_version" + androidXTestCore: "androidx.test:core:$androidx_test_core_version", + timberJUnit: "net.lachlanmckee:timber-junit-rule:$timber_junit" ] acceptanceTesting = [ diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/clipboard/Paste.kt b/domain/src/main/java/com/anytypeio/anytype/domain/clipboard/Paste.kt index 48b31a3713..9de3781e82 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/clipboard/Paste.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/clipboard/Paste.kt @@ -52,4 +52,8 @@ class Paste( val focus: Id, val range: IntRange ) + + companion object { + val DEFAULT_RANGE = IntRange(0, 0) + } } \ No newline at end of file diff --git a/presentation/build.gradle b/presentation/build.gradle index 8a717baeec..968c84ee78 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -81,4 +81,5 @@ dependencies { testImplementation unitTestDependencies.liveDataTesting testImplementation unitTestDependencies.archCoreTesting testImplementation unitTestDependencies.androidXTestCore + testImplementation unitTestDependencies.timberJUnit } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/ControlPanelMachine.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/ControlPanelMachine.kt index 73e109e56f..7f3df43627 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/ControlPanelMachine.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/ControlPanelMachine.kt @@ -615,6 +615,12 @@ sealed class ControlPanelMachine { cursorCoordinate = event.cursorCoordinate, updateList = false, items = emptyList() + ), + mainToolbar = state.mainToolbar.copy( + isVisible = false + ), + navigationToolbar = state.navigationToolbar.copy( + isVisible = false ) ) } @@ -752,7 +758,8 @@ sealed class ControlPanelMachine { ), navigationToolbar = state.navigationToolbar.copy( isVisible = false - ) + ), + slashWidget = Toolbar.SlashWidget.reset() ) is Event.SAM.OnApply -> { if (state.multiSelect.isQuickScrollAndMoveMode) { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/PageViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/PageViewModel.kt index 5303b4a00b..e089613936 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/PageViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/PageViewModel.kt @@ -37,6 +37,8 @@ import com.anytypeio.anytype.domain.block.interactor.RemoveLinkMark import com.anytypeio.anytype.domain.block.interactor.UpdateLinkMarks import com.anytypeio.anytype.domain.block.interactor.UpdateText import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes +import com.anytypeio.anytype.domain.clipboard.Copy +import com.anytypeio.anytype.domain.clipboard.Paste import com.anytypeio.anytype.domain.cover.RemoveDocCover import com.anytypeio.anytype.domain.cover.SetDocCoverImage import com.anytypeio.anytype.domain.editor.Editor @@ -1237,19 +1239,26 @@ class PageViewModel( private fun onBlockAlignmentActionClicked(alignment: Alignment) { controlPanelViewState.value?.stylingToolbar?.target?.id?.let { id -> - viewModelScope.launch { - orchestrator.proxies.intents.send( - Intent.Text.Align( - context = context, - target = id, - alignment = when (alignment) { - Alignment.START -> Block.Align.AlignLeft - Alignment.CENTER -> Block.Align.AlignCenter - Alignment.END -> Block.Align.AlignRight - } - ) + proceedWithAlignmentUpdate( + id = id, + alignment = when (alignment) { + Alignment.START -> Block.Align.AlignLeft + Alignment.CENTER -> Block.Align.AlignCenter + Alignment.END -> Block.Align.AlignRight + } + ) + } + } + + private fun proceedWithAlignmentUpdate(id: String, alignment: Block.Align) { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.Text.Align( + context = context, + target = id, + alignment = alignment ) - } + ) } } @@ -1405,30 +1414,7 @@ class PageViewModel( ActionItemType.MoveTo -> { onExitActionMode() dispatch(Command.PopBackStack) - viewModelScope.sendEvent( - analytics = analytics, - eventName = EventsDictionary.SCREEN_MOVE_TO - ) - - val excluded = mutableListOf() - - val target = blocks.find { it.id == id } - - if (target != null) { - (target.content as? Content.Link)?.let { content -> - excluded.add(content.target) - } - } - - navigate( - EventWrapper( - AppNavigation.Command.OpenMoveToScreen( - context = context, - targets = listOf(id), - excluded = excluded - ) - ) - ) + proceedWithMoveTo(id) } ActionItemType.Style -> { val textSelection = orchestrator.stores.textSelection.current() @@ -1475,6 +1461,33 @@ class PageViewModel( } } + private fun proceedWithMoveTo(id: String) { + viewModelScope.sendEvent( + analytics = analytics, + eventName = EventsDictionary.SCREEN_MOVE_TO + ) + + val excluded = mutableListOf() + + val target = blocks.find { it.id == id } + + if (target != null) { + (target.content as? Content.Link)?.let { content -> + excluded.add(content.target) + } + } + + navigate( + EventWrapper( + AppNavigation.Command.OpenMoveToScreen( + context = context, + targets = listOf(id), + excluded = excluded + ) + ) + ) + } + private fun proceedWithUnlinking(target: String) { val position = views.indexOfFirst { it.id == target } @@ -2963,7 +2976,7 @@ class PageViewModel( } fun onCopy( - range: IntRange + range: IntRange? ) { viewModelScope.launch { orchestrator.proxies.intents.send( @@ -3599,9 +3612,18 @@ class PageViewModel( //region SLASH WIDGET fun onSlashItemClicked(item: SlashItem) { + val target = orchestrator.stores.focus.current() + if (!target.isEmpty) { + proceedWithSlashItem(item, target.id) + } else { + Timber.e("Slash Widget Error, target is empty") + } + } + + private fun proceedWithSlashItem(item: SlashItem, targetId: Id) { when (item) { is SlashItem.Main.Style -> { - val items = listOf(SlashItem.Subheader.StyleWithBack) + SlashExtensions.getSlashItems() + val items = listOf(SlashItem.Subheader.StyleWithBack) + SlashExtensions.getStyleItems() onSlashCommand(SlashCommand.ShowStyleItems(items)) } is SlashItem.Main.Media -> { @@ -3643,8 +3665,16 @@ class PageViewModel( val items = listOf(SlashItem.Subheader.OtherWithBack) + SlashExtensions.getOtherItems() onSlashCommand(SlashCommand.ShowOtherItems(items)) } + is SlashItem.Main.Actions -> { + val items = listOf(SlashItem.Subheader.ActionsWithBack) + SlashExtensions.getActionItems() + onSlashCommand(SlashCommand.ShowActionItems(items)) + } + is SlashItem.Main.Alignment -> { + val items = listOf(SlashItem.Subheader.AlignmentWithBack) + SlashExtensions.getAlignmentItems() + onSlashCommand(SlashCommand.ShowAlignmentItems(items)) + } is SlashItem.Style.Type -> { - onSlashStyleTypeItemClicked(item) + onSlashStyleTypeItemClicked(item, targetId) } is SlashItem.Media -> { onSlashMediaItemClicked(item) @@ -3661,8 +3691,14 @@ class PageViewModel( controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop) addDividerBlock(style = Content.Divider.Style.DOTS) } + is SlashItem.Actions -> { + onSlashActionItemClicked(item, targetId) + } + is SlashItem.Alignment -> { + onSlashAlignmentItemClicked(item, targetId) + } else -> { - Timber.d("PRESSED ON SLAH ITEM : $item") + Timber.d("PRESSED ON SLASH ITEM : $item") } } } @@ -3692,26 +3728,82 @@ class PageViewModel( } } - private fun onSlashStyleTypeItemClicked(item: SlashItem.Style.Type) { - val target = _focus.value - if (target != null) { - viewModelScope.launch { - orchestrator.stores.focus.update( - Editor.Focus( - id = target, - cursor = Editor.Cursor.End - ) + private fun onSlashStyleTypeItemClicked(item: SlashItem.Style.Type, targetId: Id) { + viewModelScope.launch { + orchestrator.stores.focus.update( + Editor.Focus( + id = targetId, + cursor = Editor.Cursor.End ) - } - val uiBlock = item.convertToUiBlock() - controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop) - onTurnIntoBlockClicked( - target = target, - uiBlock = uiBlock ) - } else { - Timber.e("Error while style item clicked, target is null") } + val uiBlock = item.convertToUiBlock() + controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop) + onTurnIntoBlockClicked( + target = targetId, + uiBlock = uiBlock + ) + } + + private fun onSlashActionItemClicked(item: SlashItem.Actions, targetId: Id) { + when (item) { + SlashItem.Actions.CleanStyle -> { + controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop) + proceedWithClearStyle(targetId) + } + SlashItem.Actions.Copy -> { + controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop) + onCopy(range = null) + } + SlashItem.Actions.Paste -> { + val range = orchestrator.stores.textSelection.current().selection ?: Paste.DEFAULT_RANGE + controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop) + onPaste(range = range) + } + SlashItem.Actions.Delete -> { + controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop) + proceedWithUnlinking(targetId) + } + SlashItem.Actions.Duplicate -> { + controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop) + duplicateBlock(targetId) + } + SlashItem.Actions.Move -> { + mode = EditorMode.SAM + viewModelScope.launch { orchestrator.stores.focus.update(Editor.Focus.empty()) } + viewModelScope.launch { refresh() } + proceedWithSAMQuickStartSelection(targetId) + controlPanelInteractor.onEvent(ControlPanelMachine.Event.SAM.OnQuickStart(1)) + } + SlashItem.Actions.MoveTo -> { + onHideKeyboardClicked() + proceedWithMoveTo(targetId) + } + } + } + + private fun proceedWithClearStyle(targetId: Id) { + val targetBlock = blocks.first { it.id == targetId } + proceedWithUpdatingText( + Intent.Text.UpdateText( + context = context, + target = targetId, + text = targetBlock.content.asText().text, + marks = emptyList() + ) + ) + } + + private fun onSlashAlignmentItemClicked(item: SlashItem.Alignment, targetId: Id) { + val alignment = when (item) { + SlashItem.Alignment.Center -> Block.Align.AlignCenter + SlashItem.Alignment.Left -> Block.Align.AlignLeft + SlashItem.Alignment.Right -> Block.Align.AlignRight + } + proceedWithAlignmentUpdate( + id = targetId, + alignment = alignment + ) } private fun onSlashCommand(command: SlashCommand) { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/control/ControlPanelState.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/control/ControlPanelState.kt index e5b2326cf0..f66124b7e2 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/control/ControlPanelState.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/control/ControlPanelState.kt @@ -54,10 +54,10 @@ data class ControlPanelState( * @property isVisible defines whether the toolbar is visible or not */ data class Styling( + override val isVisible: Boolean, val target: Target? = null, val config: StyleConfig? = null, val props: Props? = null, - override val isVisible: Boolean, val mode: StylingMode? = null ) : Toolbar() { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/slash/SlashExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/slash/SlashExtensions.kt index 3276ebedf6..18b6765dba 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/slash/SlashExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/slash/SlashExtensions.kt @@ -40,7 +40,7 @@ object SlashExtensions { SlashItem.Main.Background, ) - fun getSlashItems() = listOf( + fun getStyleItems() = listOf( SlashItem.Style.Type.Text, SlashItem.Style.Type.Title, SlashItem.Style.Type.Heading, @@ -69,4 +69,20 @@ object SlashExtensions { SlashItem.Other.Line, SlashItem.Other.Dots ) + + fun getActionItems() = listOf( + SlashItem.Actions.Delete, + SlashItem.Actions.Duplicate, + SlashItem.Actions.Copy, + SlashItem.Actions.Paste, + SlashItem.Actions.Move, + SlashItem.Actions.MoveTo, + SlashItem.Actions.CleanStyle + ) + + fun getAlignmentItems() = listOf( + SlashItem.Alignment.Left, + SlashItem.Alignment.Center, + SlashItem.Alignment.Right + ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/slash/SlashItem.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/slash/SlashItem.kt index 716c5dae83..0a9c654e2d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/slash/SlashItem.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/slash/SlashItem.kt @@ -12,6 +12,10 @@ sealed class SlashCommand { data class ShowOtherItems(val items: List) : SlashCommand() data class ShowRelations(val relations: List): SlashCommand() data class ShowObjectTypes(val items: List): SlashCommand() + data class ShowActionItems(val items: List) : SlashCommand() + data class ShowAlignmentItems(val items: List) : SlashCommand() + data class ShowColorItems(val items: List) : SlashCommand() + data class ShowBackgroundItems(val items: List) : SlashCommand() } sealed class SlashItem { @@ -25,6 +29,14 @@ sealed class SlashItem { object ObjectType: Subheader() object Other: Subheader() object OtherWithBack: Subheader() + object Actions: Subheader() + object ActionsWithBack: Subheader() + object Alignment: Subheader() + object AlignmentWithBack: Subheader() + object Color: Subheader() + object ColorWithBack: Subheader() + object Background: Subheader() + object BackgroundWithBack: Subheader() } //endregion diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorPresentationTestSetup.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorPresentationTestSetup.kt index 91ec29488e..cff11cbc67 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorPresentationTestSetup.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorPresentationTestSetup.kt @@ -184,6 +184,8 @@ open class EditorPresentationTestSetup { private lateinit var updateDetail: UpdateDetail + open lateinit var orchestrator: Orchestrator + open fun buildViewModel(urlBuilder: UrlBuilder = builder): PageViewModel { val storage = Editor.Storage() @@ -193,6 +195,44 @@ open class EditorPresentationTestSetup { ) updateDetail = UpdateDetail(repo) + orchestrator = Orchestrator( + createBlock = createBlock, + replaceBlock = replaceBlock, + updateTextColor = updateTextColor, + duplicateBlock = duplicateBlock, + downloadFile = downloadFile, + undo = undo, + redo = redo, + updateTitle = updateTitle, + updateText = updateText, + updateCheckbox = updateCheckbox, + updateTextStyle = updateTextStyle, + updateBackgroundColor = updateBackgroundColor, + mergeBlocks = mergeBlocks, + uploadBlock = uploadBlock, + splitBlock = splitBlock, + unlinkBlocks = unlinkBlocks, + updateDivider = updateDivider, + memory = memory, + stores = storage, + proxies = proxies, + textInteractor = Interactor.TextInteractor( + proxies = proxies, + stores = storage, + matcher = DefaultPatternMatcher() + ), + updateAlignment = updateAlignment, + setupBookmark = setupBookmark, + paste = paste, + copy = copy, + move = move, + turnIntoDocument = turnIntoDocument, + analytics = analytics, + updateFields = updateFields, + setRelationKey = setRelationKey, + turnIntoStyle = turnIntoStyle + ) + return PageViewModel( getListPages = getListPages, openPage = openPage, @@ -215,43 +255,7 @@ open class EditorPresentationTestSetup { createDocument = createDocument, createNewDocument = createNewDocument, analytics = analytics, - orchestrator = Orchestrator( - createBlock = createBlock, - replaceBlock = replaceBlock, - updateTextColor = updateTextColor, - duplicateBlock = duplicateBlock, - downloadFile = downloadFile, - undo = undo, - redo = redo, - updateTitle = updateTitle, - updateText = updateText, - updateCheckbox = updateCheckbox, - updateTextStyle = updateTextStyle, - updateBackgroundColor = updateBackgroundColor, - mergeBlocks = mergeBlocks, - uploadBlock = uploadBlock, - splitBlock = splitBlock, - unlinkBlocks = unlinkBlocks, - updateDivider = updateDivider, - memory = memory, - stores = storage, - proxies = proxies, - textInteractor = Interactor.TextInteractor( - proxies = proxies, - stores = storage, - matcher = DefaultPatternMatcher() - ), - updateAlignment = updateAlignment, - setupBookmark = setupBookmark, - paste = paste, - copy = copy, - move = move, - turnIntoDocument = turnIntoDocument, - analytics = analytics, - updateFields = updateFields, - setRelationKey = setRelationKey, - turnIntoStyle = turnIntoStyle - ), + orchestrator = orchestrator, dispatcher = Dispatcher.Default(), removeDocCover = removeDocCover, setDocCoverImage = setDocCoverImage, diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorSlashWidgetActionsTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorSlashWidgetActionsTest.kt new file mode 100644 index 0000000000..c62174243e --- /dev/null +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorSlashWidgetActionsTest.kt @@ -0,0 +1,665 @@ +package com.anytypeio.anytype.presentation.page.editor + +import MockDataFactory +import android.util.Log +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.domain.block.interactor.DuplicateBlock +import com.anytypeio.anytype.domain.block.interactor.UnlinkBlocks +import com.anytypeio.anytype.domain.block.interactor.UpdateText +import com.anytypeio.anytype.domain.clipboard.Copy +import com.anytypeio.anytype.domain.clipboard.Paste +import com.anytypeio.anytype.presentation.MockTypicalDocumentFactory +import com.anytypeio.anytype.presentation.navigation.AppNavigation +import com.anytypeio.anytype.presentation.page.editor.control.ControlPanelState +import com.anytypeio.anytype.presentation.page.editor.slash.SlashEvent +import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem +import com.anytypeio.anytype.presentation.util.CoroutinesTestRule +import com.jraska.livedata.test +import net.lachlanmckee.timberjunit.TimberTestRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.times +import org.mockito.kotlin.verifyBlocking +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull + +class EditorSlashWidgetActionsTest : EditorPresentationTestSetup() { + + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @get:Rule + val timberTestRule: TimberTestRule = TimberTestRule.builder() + .minPriority(Log.DEBUG) + .showThread(true) + .showTimestamp(false) + .onlyLogWhenTestFails(true) + .build() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + //region {Action DELETE} + @Test + fun `should not hide slash widget when action delete happened`() { + + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Delete) + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + assertFalse(state.slashWidget.isVisible) + } + + @Test + fun `should send unlinkBlocks UseCase when action Delete happened`() { + + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Delete) + + val params = UnlinkBlocks.Params( + context = root, + targets = listOf(block.id) + ) + verifyBlocking(unlinkBlocks, times(1)) { invoke(params) } + } + + @Test + fun `should not triggered unlinkBlocks UseCase when no blocks in focus`() { + + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Delete) + + val params = UnlinkBlocks.Params( + context = root, + targets = listOf(block.id) + ) + verifyBlocking(unlinkBlocks, times(0)) { invoke(params) } + } + //endregion + + //region {Action DUPLICATE} + @Test + fun `should hide slash widget after action Duplicate`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Duplicate) + + val stateAfter = vm.controlPanelViewState.value + + assertNotNull(stateAfter) + assertFalse(stateAfter.slashWidget.isVisible) + assertFalse(stateAfter.navigationToolbar.isVisible) + assertFalse(stateAfter.mainToolbar.isVisible) + + val params = DuplicateBlock.Params( + context = root, + original = block.id + ) + verifyBlocking(duplicateBlock, times(1)) { invoke(params) } + } + + @Test + fun `should invoke duplicateBlock UseCase after action Duplicate`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Duplicate) + + val params = DuplicateBlock.Params( + context = root, + original = block.id + ) + verifyBlocking(duplicateBlock, times(1)) { invoke(params) } + } + //endregion + + //region {Action COPY} + @Test + fun `should hide slash widget after action Copy`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Copy) + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + assertFalse(state.slashWidget.isVisible) + } + + @Test + fun `should send Copy UseCase with null range after action Copy`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Copy) + + val params = Copy.Params( + context = root, + range = null, + blocks = listOf(block) + ) + + verifyBlocking(copy, times(1)) { invoke(params) } + } + //endregion + + //region {Action PASTE} + @Test + fun `should hide slash widget after action Paste`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Paste) + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + assertFalse(state.slashWidget.isVisible) + } + + @Test + fun `should send Paste UseCase with selection range after action Paste`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSelectionChanged( + id = block.id, + selection = IntRange(3, 3) + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Paste) + val focus = vm.focus.value + assertNotNull(focus) + + val params = Paste.Params( + context = root, + range = IntRange(3, 3), + focus = focus + ) + + verifyBlocking(paste, times(1)) { invoke(params) } + } + //endregion + + //region {Action MOVE} + @Test + fun `should hide slash widget after action Move`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Move) + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + assertFalse(state.slashWidget.isVisible) + } + + @Test + fun `should enter mode SAM after action Move`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.Move) + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + + val expected = ControlPanelState.Toolbar.MultiSelect( + isVisible = true, + isScrollAndMoveEnabled = true, + isQuickScrollAndMoveMode = true, + count = 1 + ) + + assertEquals(expected, state.multiSelect) + } + + @Test + fun `should clear focus after action Move`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + val focusBefore = orchestrator.stores.focus.current() + + assertEquals(block.id, focusBefore.id) + + vm.onSlashItemClicked(SlashItem.Actions.Move) + + val focusAfter = orchestrator.stores.focus.current() + + assertEquals("", focusAfter.id) + } + //endregion + + //region {Action MOVE TO} + @Test + fun `should hide slash widget and navigate to move to screen after move to action`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.MoveTo) + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + assertFalse(state.slashWidget.isVisible) + + val expected = AppNavigation.Command.OpenMoveToScreen( + context = root, + targets = listOf(block.id), + excluded = listOf() + ) + + vm.navigation + .test() + .assertHasValue() + .assertValue { event -> + (event.peekContent() as AppNavigation.Command.OpenMoveToScreen).let { result -> + result == expected + } + } + } + //endregion + + //region {Action CLEAN STYLE} + @Test + fun `should hide slash widget after action Clean Style`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.CleanStyle) + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + assertFalse(state.slashWidget.isVisible) + } + + @Test + fun `should send UpdateText UseCase after action Clean Style`() { + + val header = MockTypicalDocumentFactory.header + val title = MockTypicalDocumentFactory.title + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = MockDataFactory.randomString(), + marks = listOf( + Block.Content.Text.Mark( + range = IntRange(0, 5), + type = Block.Content.Text.Mark.Type.BOLD, + param = null + ), + Block.Content.Text.Mark( + range = IntRange(3, 10), + type = Block.Content.Text.Mark.Type.ITALIC, + param = null + ) + ), + style = Block.Content.Text.Style.NUMBERED + ) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, block.id) + ) + + val doc = listOf( + page, + header, + title, + block + ) + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + vm.onSlashItemClicked(SlashItem.Actions.CleanStyle) + + val params = UpdateText.Params( + context = root, + target = block.id, + text = block.content.asText().text, + marks = emptyList() + ) + + verifyBlocking(updateText, times(1)) { invoke(params) } + } + //endregion +} \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorSlashWidgetShowHideTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorSlashWidgetShowHideTest.kt new file mode 100644 index 0000000000..90b5523a83 --- /dev/null +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorSlashWidgetShowHideTest.kt @@ -0,0 +1,107 @@ +package com.anytypeio.anytype.presentation.page.editor + +import android.util.Log +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.anytypeio.anytype.presentation.MockTypicalDocumentFactory +import com.anytypeio.anytype.presentation.page.editor.slash.SlashEvent +import com.anytypeio.anytype.presentation.util.CoroutinesTestRule +import net.lachlanmckee.timberjunit.TimberTestRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.MockitoAnnotations +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class EditorSlashWidgetShowHideTest : EditorPresentationTestSetup() { + + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @get:Rule + val timberTestRule: TimberTestRule = TimberTestRule.builder() + .minPriority(Log.DEBUG) + .showThread(true) + .showTimestamp(false) + .onlyLogWhenTestFails(true) + .build() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun `should show slash widget when slash event start happened`() { + + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + assertTrue(state.slashWidget.isVisible) + } + + @Test + fun `should hide other toolbars when slash event start happened`() { + val doc = MockTypicalDocumentFactory.page(root) + val block = MockTypicalDocumentFactory.a + + stubInterceptEvents() + stubOpenDocument(document = doc) + val vm = buildViewModel() + + vm.onStart(root) + + vm.apply { + onBlockFocusChanged( + id = block.id, + hasFocus = true + ) + onSlashEvent( + SlashEvent.Start( + cursorCoordinate = 100, + slashStart = 0 + ) + ) + } + + // TESTING + + val state = vm.controlPanelViewState.value + + assertNotNull(state) + assertFalse(state.mainToolbar.isVisible) + assertFalse(state.navigationToolbar.isVisible) + assertFalse(state.mentionToolbar.isVisible) + assertFalse(state.multiSelect.isVisible) + assertFalse(state.searchToolbar.isVisible) + assertFalse(state.stylingToolbar.isVisible) + } +} \ No newline at end of file