diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt index ef2fd5885d..c90f67de22 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt @@ -127,6 +127,9 @@ abstract class TestObjectSetSetup { @Mock lateinit var storeOfObjectTypes: StoreOfObjectTypes + @Mock + lateinit var getDefaultType: GetDefaultPageType + private lateinit var getTemplates: GetTemplates private lateinit var getDefaultPageType: GetDefaultPageType @@ -242,7 +245,9 @@ abstract class TestObjectSetSetup { setQueryToObjectSet = setQueryToObjectSet, objectStore = objectStore, addObjectToCollection = addObjectToCollection, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + getDefaultPageType = getDefaultPageType, + getTemplates = getTemplates ) } diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt index 804e44c412..12f5da92d5 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt @@ -195,7 +195,9 @@ object ObjectSetModule { @Named("object-set-store") objectStore: ObjectStore, addObjectToCollection: AddObjectToCollection, convertObjectToCollection: ConvertObjectToCollection, - storeOfObjectTypes: StoreOfObjectTypes + storeOfObjectTypes: StoreOfObjectTypes, + getDefaultPageType: GetDefaultPageType, + getTemplates: GetTemplates ): ObjectSetViewModelFactory = ObjectSetViewModelFactory( openObjectSet = openObjectSet, closeBlock = closeBlock, @@ -225,7 +227,9 @@ object ObjectSetModule { objectStore = objectStore, addObjectToCollection = addObjectToCollection, objectToCollection = convertObjectToCollection, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + getDefaultPageType = getDefaultPageType, + getTemplates = getTemplates ) @JvmStatic diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt index ac9302d965..16d6b5664e 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt @@ -20,6 +20,7 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.activity.addCallback import androidx.appcompat.widget.AppCompatEditText +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.os.bundleOf @@ -30,6 +31,7 @@ import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.DividerItemDecoration @@ -46,7 +48,9 @@ import com.anytypeio.anytype.core_ui.reactive.clicks import com.anytypeio.anytype.core_ui.reactive.editorActionEvents import com.anytypeio.anytype.core_ui.reactive.touches import com.anytypeio.anytype.core_ui.tools.DefaultTextWatcher +import com.anytypeio.anytype.core_ui.views.ButtonPrimarySmallIcon import com.anytypeio.anytype.core_ui.widgets.FeaturedRelationGroupWidget +import com.anytypeio.anytype.core_ui.widgets.ObjectTypeTemplatesWidget import com.anytypeio.anytype.core_ui.widgets.StatusBadgeWidget import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.core_ui.widgets.toolbar.DataViewInfo @@ -142,6 +146,9 @@ open class ObjectSetFragment : private val addNewButton: TextView get() = binding.dataViewHeader.addNewButton + private val addNewIconButton: ButtonPrimarySmallIcon + get() = binding.dataViewHeader.addNewIconButton + private val customizeViewButton: ImageView get() = binding.dataViewHeader.customizeViewButton @@ -213,12 +220,14 @@ open class ObjectSetFragment : binding.root.setTransitionListener(transitionListener) with(lifecycleScope) { - subscribe(addNewButton.clicks().throttleFirst()) { vm.onCreateNewDataViewObject() } + subscribe(addNewButton.clicks().throttleFirst()) { vm.proceedWithCreatingNewDataViewObject() } + subscribe(addNewIconButton.buttonClicks()) { vm.proceedWithCreatingNewDataViewObject() } + subscribe(addNewIconButton.iconClicks()) { vm.onNewButtonIconClicked() } subscribe(dataViewInfo.clicks().throttleFirst()) { type -> when (type) { DataViewInfo.TYPE.COLLECTION_NO_ITEMS -> vm.onCreateObjectInCollectionClicked() DataViewInfo.TYPE.SET_NO_QUERY -> vm.onSelectQueryButtonClicked() - DataViewInfo.TYPE.SET_NO_ITEMS -> vm.onCreateNewDataViewObject() + DataViewInfo.TYPE.SET_NO_ITEMS -> vm.proceedWithCreatingNewDataViewObject() DataViewInfo.TYPE.INIT -> {} } } @@ -311,6 +320,18 @@ open class ObjectSetFragment : toast("Error while setting the Set query. The query is empty") } } + + binding.templatesWidget.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + ObjectTypeTemplatesWidget( + state = vm.templatesWidgetState.collectAsStateWithLifecycle().value, + onDismiss = vm::onDismissTemplatesWidget, + itemClick = vm::onTemplateItemClicked, + scope = lifecycleScope + ) + } + } } private fun setupWindowInsetAnimation() { @@ -432,7 +453,7 @@ open class ObjectSetFragment : header.visible() dataViewHeader.visible() viewerTitle.isEnabled = true - addNewButton.isEnabled = true + setupNewButtons(state.hasTemplates) customizeViewButton.isEnabled = true setCurrentViewerName(state.title) dataViewInfo.show(DataViewInfo.TYPE.COLLECTION_NO_ITEMS) @@ -445,7 +466,7 @@ open class ObjectSetFragment : initView.gone() dataViewHeader.visible() viewerTitle.isEnabled = true - addNewButton.isEnabled = true + setupNewButtons(state.hasTemplates) customizeViewButton.isEnabled = true setCurrentViewerName(state.viewer?.title) dataViewInfo.hide() @@ -471,12 +492,11 @@ open class ObjectSetFragment : header.visible() dataViewHeader.visible() viewerTitle.isEnabled = true - addNewButton.isEnabled = true + setupNewButtons(state.hasTemplates) customizeViewButton.isEnabled = true setCurrentViewerName(state.title) dataViewInfo.show(type = DataViewInfo.TYPE.SET_NO_ITEMS) setViewer(viewer = null) - } is DataViewViewState.Set.Default -> { topToolbarThreeDotsButton.visible() @@ -485,7 +505,7 @@ open class ObjectSetFragment : header.visible() dataViewHeader.visible() viewerTitle.isEnabled = true - addNewButton.isEnabled = true + setupNewButtons(state.hasTemplates) customizeViewButton.isEnabled = true setCurrentViewerName(state.viewer?.title) setViewer(viewer = state.viewer) @@ -521,6 +541,17 @@ open class ObjectSetFragment : } } + private fun setupNewButtons(isTemplatesAllowed: Boolean) { + if (isTemplatesAllowed) { + addNewButton.gone() + addNewIconButton.visible() + } else { + addNewButton.visible() + addNewButton.isEnabled = true + addNewIconButton.gone() + } + } + private fun setViewer(viewer: Viewer?) { when (viewer) { is Viewer.GridView -> { @@ -1063,10 +1094,16 @@ open class ObjectSetFragment : if (childFragmentManager.backStackEntryCount > 0) { childFragmentManager.popBackStack() } else { - if (vm.isCustomizeViewPanelVisible.value) { - vm.onHideViewerCustomizeSwiped() - } else { - vm.onSystemBackPressed() + when { + vm.isCustomizeViewPanelVisible.value -> { + vm.onHideViewerCustomizeSwiped() + } + vm.templatesWidgetState.value.showWidget -> { + vm.onDismissTemplatesWidget() + } + else -> { + vm.onSystemBackPressed() + } } } } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/templates/TemplateSelectAdapter.kt b/app/src/main/java/com/anytypeio/anytype/ui/templates/TemplateSelectAdapter.kt index 5714e2892d..d192c97db2 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/templates/TemplateSelectAdapter.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/templates/TemplateSelectAdapter.kt @@ -2,14 +2,14 @@ package com.anytypeio.anytype.ui.templates import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter -import com.anytypeio.anytype.presentation.templates.TemplateSelectViewModel +import com.anytypeio.anytype.presentation.templates.TemplateView class TemplateSelectAdapter( - private var items: List, + private var items: List, fragment: Fragment ) : FragmentStateAdapter(fragment) { - fun update(newItems: List) { + fun update(newItems: List) { items = newItems notifyDataSetChanged() } @@ -18,12 +18,12 @@ class TemplateSelectAdapter( override fun createFragment(position: Int): Fragment { return when (val templateView = items[position]) { - is TemplateSelectViewModel.TemplateView.Blank -> TemplateBlankFragment.new( + is TemplateView.Blank -> TemplateBlankFragment.new( typeId = templateView.typeId, typeName = templateView.typeName, layout = templateView.layout ) - is TemplateSelectViewModel.TemplateView.Template -> TemplateFragment.new( + is TemplateView.Template -> TemplateFragment.new( templateView.id ) } diff --git a/app/src/main/res/layout/fragment_object_set.xml b/app/src/main/res/layout/fragment_object_set.xml index 8ff8944b93..1be7d77918 100644 --- a/app/src/main/res/layout/fragment_object_set.xml +++ b/app/src/main/res/layout/fragment_object_set.xml @@ -148,4 +148,12 @@ android:visibility="gone" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_object_set_scene.xml b/app/src/main/res/xml/fragment_object_set_scene.xml index ffdd8dd019..79899fbcc8 100644 --- a/app/src/main/res/xml/fragment_object_set_scene.xml +++ b/app/src/main/res/xml/fragment_object_set_scene.xml @@ -1,6 +1,7 @@ + xmlns:motion="http://schemas.android.com/apk/res-auto" + xmlns:app="http://schemas.android.com/tools"> + + + @@ -171,6 +178,13 @@ motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="parent" motion:visibilityMode="ignore" /> + + + + \ No newline at end of file diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectTypeIds.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectTypeIds.kt index 683078f3a8..a38947dd99 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectTypeIds.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectTypeIds.kt @@ -25,6 +25,25 @@ object ObjectTypeIds { const val SPACE = "ot-space" const val DEFAULT_OBJECT_TYPE_PREFIX = "ot-" + + fun getTypesWithoutTemplates(): List = + listOf(BOOKMARK, NOTE).plus(getFileTypes()).plus(getSetTypes()) + .plus(getSystemTypes()) + + fun getFileTypes(): List = listOf(FILE, IMAGE, AUDIO, VIDEO) + + fun getSystemTypes(): List = listOf( + OBJECT_TYPE, + TEMPLATE, + RELATION, + RELATION_OPTION, + DASHBOARD, + DATE, + MarketplaceObjectTypeIds.OBJECT_TYPE, + MarketplaceObjectTypeIds.RELATION + ) + + fun getSetTypes(): List = listOf(SET, COLLECTION) } object MarketplaceObjectTypeIds { diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/DesignSystemButtons.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/DesignSystemButtons.kt index 74d78fa70f..c3d998fd56 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/DesignSystemButtons.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/DesignSystemButtons.kt @@ -2,6 +2,10 @@ package com.anytypeio.anytype.core_ui.views import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView import androidx.appcompat.widget.AppCompatTextView import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke @@ -33,8 +37,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.reactive.clicks import com.anytypeio.anytype.core_ui.views.animations.DotsLoadingIndicator import com.anytypeio.anytype.core_ui.views.animations.FadeAnimationSpecs +import com.anytypeio.anytype.core_utils.ext.throttleFirst class ButtonPrimaryXSmall @JvmOverloads constructor( context: Context, @@ -563,4 +569,30 @@ fun MyWarningButton() { .fillMaxWidth() .padding(start = 16.dp, end = 16.dp) ) +} + +class ButtonPrimarySmallIcon @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : LinearLayout(context, attrs) { + + private lateinit var button: TextView + private lateinit var icon: ImageView + + init { + setup(context) + } + + private fun setup(context: Context) { + LayoutInflater.from(context).inflate(R.layout.ds_button_icon, this, true) + button = findViewById(R.id.button) + icon = findViewById(R.id.icon) + } + + fun setButtonText(text: String) { + button.text = text + } + + fun buttonClicks() = button.clicks().throttleFirst() + fun iconClicks() = icon.clicks().throttleFirst() } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt index e8106007ef..ea57bc96a6 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt @@ -267,6 +267,14 @@ val Caption2Regular = TextStyle( letterSpacing = (-0.006).em ) +val Caption2Semibold = TextStyle( + fontFamily = fontInterSemibold, + fontWeight = FontWeight.W600, + fontSize = 11.sp, + lineHeight = 14.sp, + letterSpacing = (-0.006).em +) + //UX/Button/Medium val ButtonMedium = TextStyle( fontFamily = fontInterMedium, diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectTypeTemplatesWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectTypeTemplatesWidget.kt new file mode 100644 index 0000000000..8ba02ad577 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectTypeTemplatesWidget.kt @@ -0,0 +1,332 @@ +package com.anytypeio.anytype.core_ui.widgets + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FractionalThreshold +import androidx.compose.material.Text +import androidx.compose.material.rememberSwipeableState +import androidx.compose.material.swipeable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.foundation.noRippleClickable +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular +import com.anytypeio.anytype.core_ui.views.Caption2Semibold +import com.anytypeio.anytype.core_ui.views.Title1 +import com.anytypeio.anytype.presentation.templates.TemplateView +import com.anytypeio.anytype.presentation.widgets.TemplatesWidgetUiState +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ObjectTypeTemplatesWidget( + state: TemplatesWidgetUiState, + onDismiss: () -> Unit, + itemClick: (TemplateView) -> Unit, + scope: CoroutineScope +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + + val currentState by rememberUpdatedState(state) + val swipeableState = rememberSwipeableState(DragStates.VISIBLE) + + AnimatedVisibility( + visible = currentState.showWidget, + enter = fadeIn(), + exit = fadeOut(tween(200) + ) + ) { + Box( + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.4f)) + .noRippleClickable { onDismiss() } + ) + } + + if (swipeableState.isAnimationRunning) { + DisposableEffect(Unit) { + onDispose { + onDismiss() + } + } + } + + if (!currentState.showWidget) { + DisposableEffect(Unit) { + onDispose { + scope.launch { swipeableState.snapTo(DragStates.VISIBLE) } + } + } + } + + val sizePx = with(LocalDensity.current) { 312.dp.toPx() } + + AnimatedVisibility( + visible = currentState.showWidget, + enter = slideInVertically { it }, + exit = slideOutVertically(tween(200)) { it }, + modifier = Modifier + .swipeable( + state = swipeableState, + orientation = Orientation.Vertical, + anchors = mapOf( + 0f to DragStates.VISIBLE, + sizePx to DragStates.DISMISSED + ), + thresholds = { _, _ -> FractionalThreshold(0.3f) } + ) + .offset { IntOffset(0, swipeableState.offset.value.roundToInt()) } + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(312.dp) + .padding(start = 8.dp, end = 8.dp, bottom = 31.dp) + .background( + color = colorResource(id = R.color.background_primary), + shape = RoundedCornerShape(size = 16.dp) + ) + , + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(bottom = 24.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) { +// Box( +// modifier = Modifier +// .align(Alignment.CenterStart), +// ) { +// Text( +// modifier = Modifier.padding( +// start = 16.dp, +// top = 12.dp, +// bottom = 12.dp, +// end = 16.dp +// ), +// text = stringResource(id = R.string.edit), +// style = BodyCalloutRegular +// ) +// } + Box(modifier = Modifier.align(Alignment.Center)) { + Text( + text = stringResource(R.string.type_templates_widget_title), + style = Title1, + color = colorResource(R.color.text_primary) + ) + } +// Box(modifier = Modifier.align(Alignment.CenterEnd)) { +// Image( +// modifier = Modifier.padding( +// start = 16.dp, +// top = 12.dp, +// bottom = 12.dp, +// end = 16.dp +// ), +// painter = painterResource(id = R.drawable.ic_default_plus), +// contentDescription = null +// ) +// } + } + TemplatesList(currentState.items) { + scope.launch { + onDismiss() + delay(200L) + itemClick(it) + } + } + } + } + } + } +} + +@Composable +private fun TemplatesList( + items: List, + itemClick: (TemplateView) -> Unit +) { + LazyRow( + modifier = Modifier + .padding(top = 8.dp) + .height(224.dp) + .fillMaxWidth(), + contentPadding = PaddingValues(start = 16.dp, end = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) + { + itemsIndexed( + items = items, + itemContent = { index, item -> + Box( + modifier = Modifier + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_primary), + shape = RoundedCornerShape(size = 16.dp) + ) + .height(224.dp) + .width(120.dp) + .clickable { + itemClick(item) + } + ) { + Column { + TemplateItemContent(item) + } + } + } + ) + } +} + +@Composable +private fun TemplateItemContent(item: TemplateView) { + when (item) { + is TemplateView.Blank -> { + Spacer(modifier = Modifier.height(28.dp)) + TemplateItemTitle(text = stringResource(id = R.string.blank)) + } + is TemplateView.Template -> { + Spacer(modifier = Modifier.height(28.dp)) + TemplateItemTitle(text = item.name) + Spacer(modifier = Modifier.height(12.dp)) + TemplateItemRectangles() + } + } +} + +@Composable +private fun TemplateItemTitle(text: String) { + Text( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp + ), + text = text, + style = Caption2Semibold.copy( + color = colorResource(id = R.color.text_primary) + ), + ) +} + +@Composable +private fun TemplateItemRectangles() { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .padding(start = 16.dp, end = 16.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(6.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .padding(start = 16.dp, end = 16.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(6.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .padding(start = 16.dp, end = 40.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + } +} + +private enum class DragStates { + VISIBLE, + DISMISSED +} + + +@Preview +@Composable +fun ComposablePreview() { + val items = listOf( + TemplateView.Blank( + typeId = "page", + typeName = "Page", + layout = ObjectType.Layout.BASIC.code + ), + TemplateView.Template( + id = "1", + name = "Template 1", + typeId = "page", + layout = ObjectType.Layout.BASIC, + image = null, + emoji = null + ), + ) + val state = TemplatesWidgetUiState(items = items, showWidget = true) + ObjectTypeTemplatesWidget(state = state, onDismiss = {}, itemClick = {}, scope = CoroutineScope( + Dispatchers.Main + ) + ) +} \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/ic_arrow_down_18.xml b/core-ui/src/main/res/drawable/ic_arrow_down_18.xml new file mode 100644 index 0000000000..9c5c1867f6 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_arrow_down_18.xml @@ -0,0 +1,12 @@ + + + diff --git a/core-ui/src/main/res/layout/ds_button_icon.xml b/core-ui/src/main/res/layout/ds_button_icon.xml new file mode 100644 index 0000000000..2adb982243 --- /dev/null +++ b/core-ui/src/main/res/layout/ds_button_icon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/layout_object_set_dv_header.xml b/core-ui/src/main/res/layout/layout_object_set_dv_header.xml index 1ecea11624..4fb74d61cf 100644 --- a/core-ui/src/main/res/layout/layout_object_set_dv_header.xml +++ b/core-ui/src/main/res/layout/layout_object_set_dv_header.xml @@ -25,29 +25,15 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="center_vertical" - android:layout_marginEnd="@dimen/dp_16" android:contentDescription="@string/content_description_customize_view_button" android:paddingStart="@dimen/dp_20" + android:paddingEnd="@dimen/dp_8" android:src="@drawable/bg_viewer_settings_icon_bg" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/addNewButton" + app:layout_constraintEnd_toStartOf="@+id/barrier" app:layout_constraintTop_toTopOf="parent" tools:enabled="true" /> - - + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/widget_data_view_customize_view.xml b/core-ui/src/main/res/layout/widget_data_view_customize_view.xml index 00a1ca83a5..eccad13f3d 100644 --- a/core-ui/src/main/res/layout/widget_data_view_customize_view.xml +++ b/core-ui/src/main/res/layout/widget_data_view_customize_view.xml @@ -1,5 +1,4 @@ - #FFFFFF #24DAD7CA + #1A000000 diff --git a/core-ui/src/main/res/values/colors.xml b/core-ui/src/main/res/values/colors.xml index 6307821b1a..c9269fdbbc 100644 --- a/core-ui/src/main/res/values/colors.xml +++ b/core-ui/src/main/res/values/colors.xml @@ -210,5 +210,6 @@ #000000 #E3E3E3 + #1AFFFFFF diff --git a/core-ui/src/main/res/values/strings.xml b/core-ui/src/main/res/values/strings.xml index e44e8d872d..1ee4faa0aa 100644 --- a/core-ui/src/main/res/values/strings.xml +++ b/core-ui/src/main/res/values/strings.xml @@ -643,5 +643,7 @@ Open set of %1$s Create set of %1$s Current view cannot be deleted + Select template + Blank diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/CreateDataViewObject.kt b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/CreateDataViewObject.kt index 504202f097..b30ab4227b 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/CreateDataViewObject.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/CreateDataViewObject.kt @@ -33,12 +33,12 @@ class CreateDataViewObject( return when (params) { is Params.SetByType -> { val command = Command.CreateObject( - template = resolveTemplateForNewObject(type = params.type), + template = params.template, prefilled = resolveSetByTypePrefilledObjectData( filters = params.filters, type = params.type ), - internalFlags = listOf(InternalFlags.ShouldSelectTemplate) + internalFlags = listOf() ) val result = repo.createObject(command) Result( @@ -49,16 +49,13 @@ class CreateDataViewObject( is Params.SetByRelation -> { val type = resolveDefaultObjectType() val command = Command.CreateObject( - template = resolveTemplateForNewObject(type = type), + template = params.template, prefilled = resolveSetByRelationPrefilledObjectData( filters = params.filters, relations = params.relations, type = type ), - internalFlags = listOf( - InternalFlags.ShouldSelectType, - InternalFlags.ShouldSelectTemplate - ) + internalFlags = listOf() ) val result = repo.createObject(command) Result( @@ -66,19 +63,16 @@ class CreateDataViewObject( objectType = type ) } - Params.Collection -> { + is Params.Collection -> { val type = resolveDefaultObjectType() val command = Command.CreateObject( - template = resolveTemplateForNewObject(type = type), + template = params.templateId, prefilled = resolveSetByRelationPrefilledObjectData( filters = emptyList(), relations = emptyList(), type = type ), - internalFlags = listOf( - InternalFlags.ShouldSelectType, - InternalFlags.ShouldSelectTemplate - ) + internalFlags = listOf() ) val result = repo.createObject(command) Result( @@ -180,15 +174,19 @@ class CreateDataViewObject( sealed class Params { data class SetByType( val type: Id, - val filters: List + val filters: List, + val template: Id? ) : Params() data class SetByRelation( val filters: List, - val relations: List + val relations: List, + val template: Id? ) : Params() - object Collection : Params() + data class Collection( + val templateId: Id? + ) : Params() } data class Result( diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/templates/GetTemplates.kt b/domain/src/main/java/com/anytypeio/anytype/domain/templates/GetTemplates.kt index bd101329c8..f502eef703 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/templates/GetTemplates.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/templates/GetTemplates.kt @@ -1,7 +1,9 @@ package com.anytypeio.anytype.domain.templates +import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.DVFilter import com.anytypeio.anytype.core_models.DVFilterCondition +import com.anytypeio.anytype.core_models.DVSort import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeIds @@ -10,7 +12,6 @@ import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.base.ResultInteractor import com.anytypeio.anytype.domain.block.repo.BlockRepository -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext /** @@ -28,13 +29,13 @@ class GetTemplates( filters = listOf( DVFilter( relation = Relations.IS_ARCHIVED, - condition = DVFilterCondition.EQUAL, - value = false + condition = DVFilterCondition.NOT_EQUAL, + value = true ), DVFilter( relation = Relations.IS_DELETED, - condition = DVFilterCondition.EQUAL, - value = false + condition = DVFilterCondition.NOT_EQUAL, + value = true ), DVFilter( relation = Relations.TYPE, @@ -47,11 +48,25 @@ class GetTemplates( value = params.type ) ), - keys = listOf(Relations.ID, Relations.NAME), - sorts = emptyList(), + keys = listOf( + Relations.ID, + Relations.NAME, + Relations.LAYOUT, + Relations.ICON_EMOJI, + Relations.ICON_IMAGE, + Relations.ICON_OPTION, + Relations.COVER_ID, + Relations.COVER_TYPE + ), + sorts = listOf( + DVSort( + relationKey = Relations.CREATED_DATE, + type = Block.Content.DataView.Sort.Type.DESC + ) + ), fulltext = "", offset = 0, - limit = 0 + limit = 100 ).map { obj -> ObjectWrapper.Basic(obj) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index faecd732ea..13d1681674 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlinVersion = '1.7.10' androidxCoreVersion = "1.10.1" -androidxComposeVersion = '1.3.1' +androidxComposeVersion = '1.4.3' composeKotlinCompilerVersion = '1.3.1' composeMaterial3Version = '1.1.1' composeMaterialVersion = '1.3.1' diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectTypeExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectTypeExtensions.kt index 53fe4f4090..c866992467 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectTypeExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectTypeExtensions.kt @@ -1,10 +1,12 @@ package com.anytypeio.anytype.presentation.objects +import com.anytypeio.anytype.core_models.ObjectTypeIds import com.anytypeio.anytype.core_models.ObjectTypeIds.BOOKMARK import com.anytypeio.anytype.core_models.ObjectTypeIds.COLLECTION import com.anytypeio.anytype.core_models.ObjectTypeIds.SET import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.presentation.mapper.toObjectTypeView +import com.anytypeio.anytype.presentation.objects.SupportedLayouts.editorLayouts /** * The method allows you to get object type views for using in the editor and set @@ -49,4 +51,16 @@ fun List.getObjectTypeViewsForSBPage( return@forEach } return result.sortedWith(ObjectTypeViewComparator()) +} + +/** + * + * This method is used to understand if objects of this type can use templates. + * + * @return `true` if templates are allowed for this type of object, `false` otherwise. + */ +fun ObjectWrapper.Type.isTemplatesAllowed(): Boolean { + val showTemplates = !ObjectTypeIds.getTypesWithoutTemplates().contains(this.id) + val allowedObject = editorLayouts.contains(recommendedLayout) + return showTemplates && allowedObject } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/DataViewViewState.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/DataViewViewState.kt index bf9e420061..861fb01028 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/DataViewViewState.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/DataViewViewState.kt @@ -7,15 +7,15 @@ sealed class DataViewViewState { sealed class Collection : DataViewViewState() { object NoView : Collection() - data class NoItems(val title: String) : Collection() - data class Default(val viewer: Viewer?) : Collection() + data class NoItems(val title: String, val hasTemplates: Boolean = false) : Collection() + data class Default(val viewer: Viewer?, val hasTemplates: Boolean = false) : Collection() } sealed class Set : DataViewViewState() { object NoQuery : Set() object NoView : Set() - data class NoItems(val title: String) : Set() - data class Default(val viewer: Viewer?) : Set() + data class NoItems(val title: String, val hasTemplates: Boolean) : Set() + data class Default(val viewer: Viewer?, val hasTemplates: Boolean) : Set() } object Init: DataViewViewState() diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt index 7b7ff97dd0..ae896aaf8d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt @@ -11,6 +11,7 @@ import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVSor import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVViewerFields import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVViewerRelationUpdate import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeIds import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.RelationFormat @@ -20,22 +21,26 @@ import com.anytypeio.anytype.core_utils.ext.addAfterIndexInLine import com.anytypeio.anytype.core_utils.ext.mapInPlace import com.anytypeio.anytype.core_utils.ext.moveAfterIndexInLine import com.anytypeio.anytype.core_utils.ext.moveOnTop +import com.anytypeio.anytype.domain.launch.GetDefaultPageType import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider import com.anytypeio.anytype.presentation.editor.editor.model.BlockView import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.objects.getProperName +import com.anytypeio.anytype.presentation.objects.isTemplatesAllowed import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.relations.ObjectSetConfig.ID_KEY import com.anytypeio.anytype.presentation.relations.isSystemKey import com.anytypeio.anytype.presentation.relations.title +import com.anytypeio.anytype.presentation.relations.type import com.anytypeio.anytype.presentation.relations.view import com.anytypeio.anytype.presentation.sets.model.ObjectView import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView import com.anytypeio.anytype.presentation.sets.model.Viewer import com.anytypeio.anytype.presentation.sets.state.ObjectState -import timber.log.Timber +import com.anytypeio.anytype.presentation.templates.TemplateView fun ObjectState.DataView.featuredRelations( ctx: Id, @@ -370,6 +375,35 @@ fun ObjectState.DataView.filterOutDeletedAndMissingObjects(query: List): Lis return query.filter(::isValidObject) } +suspend fun ObjectState.DataView.Set.isTemplatesAllowed( + setOfValue: List, + storeOfObjectTypes: StoreOfObjectTypes, + getDefaultPageType: GetDefaultPageType +): Boolean { + val objectDetails = details[setOfValue.first()]?.map.orEmpty() + return when (objectDetails.type){ + ObjectTypeIds.OBJECT_TYPE -> { + val objectWrapper = ObjectWrapper.Type(objectDetails) + objectWrapper.isTemplatesAllowed() + } + ObjectTypeIds.RELATION -> { + //We have set of relations, need to check default object type + storeOfObjectTypes.isTemplatesAllowedForDefaultType(getDefaultPageType) + } + else -> false + } +} + +suspend fun StoreOfObjectTypes.isTemplatesAllowedForDefaultType(getDefaultPageType: GetDefaultPageType): Boolean { + try { + val defaultObjectType = getDefaultPageType.run(Unit).type ?: return false + val defaultObjType = get(defaultObjectType) ?: return false + return defaultObjType.isTemplatesAllowed() + } catch (e: Exception){ + return false + } +} + private fun ObjectState.DataView.isValidObject(objectId: Id): Boolean { val objectDetails = details[objectId] ?: return false val objectWrapper = ObjectWrapper.Basic(objectDetails.map) @@ -394,4 +428,22 @@ fun Viewer.isEmpty(): Boolean = is Viewer.GridView -> this.rows.isEmpty() is Viewer.ListView -> this.items.isEmpty() is Viewer.Unsupported -> false - } \ No newline at end of file + } + +fun ObjectWrapper.Basic.toTemplateView(typeId: Id): TemplateView.Template { + return TemplateView.Template( + id = id, + name = name.orEmpty(), + typeId = typeId, + emoji = iconEmoji, + image = iconImage, + layout = layout ?: ObjectType.Layout.BASIC + ) +} + +fun ObjectWrapper.Basic.toTemplateViewBlank(typeId: Id): TemplateView.Blank { + return TemplateView.Blank( + typeId = typeId, + layout = layout?.code ?: ObjectType.Layout.BASIC.code + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt index 85fa9f3ec5..c5c70220f0 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt @@ -26,6 +26,7 @@ import com.anytypeio.anytype.domain.cover.SetDocCoverImage import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewObject import com.anytypeio.anytype.domain.error.Error import com.anytypeio.anytype.domain.event.interactor.InterceptEvents +import com.anytypeio.anytype.domain.launch.GetDefaultPageType import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.`object`.ConvertObjectToCollection import com.anytypeio.anytype.domain.`object`.UpdateDetail @@ -40,6 +41,7 @@ import com.anytypeio.anytype.domain.search.DataViewSubscriptionContainer import com.anytypeio.anytype.domain.sets.OpenObjectSet import com.anytypeio.anytype.domain.sets.SetQueryToObjectSet import com.anytypeio.anytype.domain.status.InterceptThreadStatus +import com.anytypeio.anytype.domain.templates.GetTemplates import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage import com.anytypeio.anytype.domain.workspace.WorkspaceManager import com.anytypeio.anytype.presentation.common.Action @@ -54,6 +56,7 @@ import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEve import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent import com.anytypeio.anytype.presentation.navigation.AppNavigation import com.anytypeio.anytype.presentation.navigation.SupportNavigation +import com.anytypeio.anytype.presentation.objects.isTemplatesAllowed import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.relations.ObjectSetConfig.DEFAULT_LIMIT import com.anytypeio.anytype.presentation.relations.RelationListViewModel @@ -63,7 +66,9 @@ import com.anytypeio.anytype.presentation.sets.model.Viewer import com.anytypeio.anytype.presentation.sets.state.ObjectState import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer import com.anytypeio.anytype.presentation.sets.subscription.DataViewSubscription +import com.anytypeio.anytype.presentation.templates.TemplateView import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.presentation.widgets.TemplatesWidgetUiState import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -116,7 +121,9 @@ class ObjectSetViewModel( private val objectStore: ObjectStore, private val addObjectToCollection: AddObjectToCollection, private val objectToCollection: ConvertObjectToCollection, - private val storeOfObjectTypes: StoreOfObjectTypes + private val storeOfObjectTypes: StoreOfObjectTypes, + private val getDefaultPageType: GetDefaultPageType, + private val getTemplates: GetTemplates ) : ViewModel(), SupportNavigation> { val status = MutableStateFlow(SyncStatus.UNKNOWN) @@ -144,12 +151,15 @@ class ObjectSetViewModel( MutableStateFlow(DataViewViewState.Init) val currentViewer = _currentViewer + private val _templateViews = MutableStateFlow>(emptyList()) + private val _header = MutableStateFlow( SetOrCollectionHeaderState.None ) val header: StateFlow = _header val isCustomizeViewPanelVisible = MutableStateFlow(false) + val templatesWidgetState = MutableStateFlow(TemplatesWidgetUiState.empty()) @Deprecated("could be deleted") val isLoading = MutableStateFlow(false) @@ -173,6 +183,7 @@ class ObjectSetViewModel( urlBuilder = urlBuilder, coverImageHashProvider = coverImageHashProvider ) + gettingTemplatesForDataViewState(state) } } @@ -405,9 +416,10 @@ class ObjectSetViewModel( combine( database.index, stateReducer.state, - session.currentViewerId - ) { dataViewState, objectState, currentViewId -> - processViewState(dataViewState, objectState, currentViewId) + session.currentViewerId, + _templateViews + ) { dataViewState, objectState, currentViewId, templates -> + processViewState(dataViewState, objectState, currentViewId, templates) }.distinctUntilChanged().collect { viewState -> Timber.d("subscribeToDataViewViewer, newViewerState:[$viewState]") _currentViewer.value = viewState @@ -418,18 +430,21 @@ class ObjectSetViewModel( private suspend fun processViewState( dataViewState: DataViewState, objectState: ObjectState, - currentViewId: String? + currentViewId: String?, + templates: List ): DataViewViewState { return when (objectState) { is ObjectState.DataView.Collection -> processCollectionState( - dataViewState, - objectState, - currentViewId + dataViewState = dataViewState, + objectState = objectState, + currentViewId = currentViewId, + templates = templates ) is ObjectState.DataView.Set -> processSetState( - dataViewState, - objectState, - currentViewId + dataViewState = dataViewState, + objectState = objectState, + currentViewId = currentViewId, + templates = templates ) ObjectState.Init -> DataViewViewState.Init ObjectState.ErrorLayout -> DataViewViewState.Error(msg = "Wrong layout, couldn't open object") @@ -439,7 +454,8 @@ class ObjectSetViewModel( private suspend fun processCollectionState( dataViewState: DataViewState, objectState: ObjectState.DataView.Collection, - currentViewId: String? + currentViewId: String?, + templates: List ): DataViewViewState { if (!objectState.isInitialized) return DataViewViewState.Init @@ -461,8 +477,26 @@ class ObjectSetViewModel( when { viewer == null -> DataViewViewState.Collection.NoView - viewer.isEmpty() -> DataViewViewState.Collection.NoItems(title = viewer.title) - else -> DataViewViewState.Collection.Default(viewer = viewer) + viewer.isEmpty() -> { + val isTemplatesPresent = templates.isNotEmpty() && + storeOfObjectTypes.isTemplatesAllowedForDefaultType( + getDefaultPageType = getDefaultPageType + ) + DataViewViewState.Collection.NoItems( + title = viewer.title, + hasTemplates = isTemplatesPresent + ) + } + else -> { + val isTemplatesPresent = templates.isNotEmpty() && + storeOfObjectTypes.isTemplatesAllowedForDefaultType( + getDefaultPageType = getDefaultPageType + ) + DataViewViewState.Collection.Default( + viewer = viewer, + hasTemplates = isTemplatesPresent + ) + } } } } @@ -471,7 +505,8 @@ class ObjectSetViewModel( private suspend fun processSetState( dataViewState: DataViewState, objectState: ObjectState.DataView.Set, - currentViewId: String? + currentViewId: String?, + templates: List ): DataViewViewState { if (!objectState.isInitialized) return DataViewViewState.Init @@ -503,8 +538,30 @@ class ObjectSetViewModel( when { query.isEmpty() || setOfValue.isEmpty() -> DataViewViewState.Set.NoQuery render == null -> DataViewViewState.Set.NoView - render.isEmpty() -> DataViewViewState.Set.NoItems(title = render.title) - else -> DataViewViewState.Set.Default(viewer = render) + render.isEmpty() -> { + val isTemplatesAllowed = templates.isNotEmpty() && + objectState.isTemplatesAllowed( + setOfValue, + storeOfObjectTypes, + getDefaultPageType + ) + DataViewViewState.Set.NoItems( + title = render.title, + hasTemplates = isTemplatesAllowed + ) + } + else -> { + val isTemplatesAllowed = templates.isNotEmpty() && + objectState.isTemplatesAllowed( + setOfValue, + storeOfObjectTypes, + getDefaultPageType + ) + DataViewViewState.Set.Default( + viewer = render, + hasTemplates = isTemplatesAllowed + ) + } } } } @@ -810,16 +867,29 @@ class ObjectSetViewModel( } } - fun onCreateNewDataViewObject() { - Timber.d("onCreateNewRecord, ") + fun proceedWithCreatingNewDataViewObject(templatesId: Id? = null) { + Timber.d("proceedWithCreatingNewDataViewObject, templatesId:[$templatesId]") val state = stateReducer.state.value.dataViewState() ?: return when (state) { - is ObjectState.DataView.Collection -> proceedWithAddingObjectToCollection() - is ObjectState.DataView.Set -> proceedWithCreatingSetObject(state) + is ObjectState.DataView.Collection -> proceedWithAddingObjectToCollection(templatesId) + is ObjectState.DataView.Set -> proceedWithCreatingSetObject(state, templatesId) } } - private fun proceedWithCreatingSetObject(currentState: ObjectState.DataView.Set) { + fun onNewButtonIconClicked() { + Timber.d("onNewButtonIconClicked, ") + templatesWidgetState.value = TemplatesWidgetUiState( + items = _templateViews.value, + showWidget = true + ) + } + + fun onDismissTemplatesWidget() { + Timber.d("onDismissTemplatesWidget, ") + templatesWidgetState.value = templatesWidgetState.value.copy(showWidget = false) + } + + private fun proceedWithCreatingSetObject(currentState: ObjectState.DataView.Set, templateId: Id?) { if (isRestrictionPresent(DataViewRestriction.CREATE_OBJECT)) { toast(NOT_ALLOWED) } else { @@ -849,7 +919,8 @@ class ObjectSetViewModel( proceedWithCreatingDataViewObject( CreateDataViewObject.Params.SetByType( type = sourceId, - filters = viewer.filters + filters = viewer.filters, + template = templateId ) ) } @@ -858,7 +929,8 @@ class ObjectSetViewModel( proceedWithCreatingDataViewObject( CreateDataViewObject.Params.SetByRelation( filters = viewer.filters, - relations = setObject.setOf + relations = setObject.setOf, + template = templateId ) ) } @@ -879,8 +951,11 @@ class ObjectSetViewModel( proceedWithAddingObjectToCollection() } - private fun proceedWithAddingObjectToCollection() { - proceedWithCreatingDataViewObject(CreateDataViewObject.Params.Collection) { result -> + private fun proceedWithAddingObjectToCollection(templateId: Id? = null) { + val createObjectParams = CreateDataViewObject.Params.Collection( + templateId = templateId + ) + proceedWithCreatingDataViewObject(createObjectParams) { result -> val params = AddObjectToCollection.Params( ctx = context, after = "", @@ -917,7 +992,7 @@ class ObjectSetViewModel( private suspend fun proceedWithNewDataViewObject(params: CreateDataViewObject.Params, newObject: Id) { when (params) { - CreateDataViewObject.Params.Collection -> { + is CreateDataViewObject.Params.Collection -> { proceedWithOpeningObject(newObject) } is CreateDataViewObject.Params.SetByRelation -> { @@ -1391,6 +1466,84 @@ class ObjectSetViewModel( } } + // region TEMPLATES + private suspend fun gettingTemplatesForDataViewState(state: ObjectState.DataView) { + when (state) { + is ObjectState.DataView.Collection -> { + proceedWithGettingTemplates(typeId = null) + } + is ObjectState.DataView.Set -> { + val sourceId = proceedWithGettingSetSourceId(state) + val sourceMap = state.details[sourceId] ?: return + when (sourceMap.type.firstOrNull()) { + ObjectTypeIds.RELATION -> proceedWithGettingTemplates(typeId = null) + ObjectTypeIds.OBJECT_TYPE -> proceedWithGettingTemplates(typeId = sourceId) + else -> { Timber.d("Ignoring type of source") } + } + } + } + } + + private suspend fun proceedWithGettingTemplates(typeId: Id?) { + val objectType = resolveObjectType(typeId) + if (objectType?.isTemplatesAllowed() == true) { + viewModelScope.launch { + getTemplates.async(GetTemplates.Params(objectType.id)).fold( + onSuccess = { templates -> + if (templates.isNotEmpty()) { + _templateViews.value = + listOf(templates.first().toTemplateViewBlank(objectType.id)) + + templates.map { it.toTemplateView(typeId = objectType.id) } + } + }, + onFailure = { e -> + Timber.e(e, "Error getting templates for type ${objectType.id}") + } + ) + } + } else { + Timber.d("Templates are not allowed for type:[${objectType?.id}]") + } + } + + private suspend fun resolveObjectType(type: Id?): ObjectWrapper.Type? { + return if (type == null) { + val defaultObjectType = getDefaultPageType.run(Unit).type ?: return null + storeOfObjectTypes.get(defaultObjectType) + } else { + storeOfObjectTypes.get(type) + } + } + + fun onTemplateItemClicked(item: TemplateView) { + when(item) { + is TemplateView.Blank -> { + templatesWidgetState.value = TemplatesWidgetUiState.empty() + proceedWithCreatingNewDataViewObject() + } + is TemplateView.Template -> { + templatesWidgetState.value = TemplatesWidgetUiState.empty() + proceedWithCreatingNewDataViewObject(templatesId = item.id) + } + } + } + + private fun proceedWithGettingSetSourceId(currentState: ObjectState.DataView.Set): Id? { + if (isRestrictionPresent(DataViewRestriction.CREATE_OBJECT) || !currentState.isInitialized) { + toast(NOT_ALLOWED) + return null + } + + val setObject = ObjectWrapper.Basic(currentState.details[context]?.map ?: emptyMap()) + val sourceId = setObject.setOf.singleOrNull() + if (sourceId == null) { + Timber.e("Unable to define a source for a new object.") + toast("Unable to define a source for a new object.") + } + return sourceId + } + //endregion + companion object { const val NOT_ALLOWED = "Not allowed for this set" const val NOT_ALLOWED_CELL = "Not allowed for this cell" diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt index 3f33aef044..43034fb6ea 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt @@ -10,6 +10,7 @@ import com.anytypeio.anytype.domain.collections.AddObjectToCollection import com.anytypeio.anytype.domain.cover.SetDocCoverImage import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewObject import com.anytypeio.anytype.domain.event.interactor.InterceptEvents +import com.anytypeio.anytype.domain.launch.GetDefaultPageType import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.`object`.ConvertObjectToCollection import com.anytypeio.anytype.domain.objects.ObjectStore @@ -22,6 +23,7 @@ import com.anytypeio.anytype.domain.search.DataViewSubscriptionContainer import com.anytypeio.anytype.domain.sets.OpenObjectSet import com.anytypeio.anytype.domain.sets.SetQueryToObjectSet import com.anytypeio.anytype.domain.status.InterceptThreadStatus +import com.anytypeio.anytype.domain.templates.GetTemplates import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage import com.anytypeio.anytype.domain.workspace.WorkspaceManager import com.anytypeio.anytype.presentation.common.Action @@ -60,7 +62,9 @@ class ObjectSetViewModelFactory( private val objectStore: ObjectStore, private val addObjectToCollection: AddObjectToCollection, private val objectToCollection: ConvertObjectToCollection, - private val storeOfObjectTypes: StoreOfObjectTypes + private val storeOfObjectTypes: StoreOfObjectTypes, + private val getDefaultPageType: GetDefaultPageType, + private val getTemplates: GetTemplates ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { @@ -93,7 +97,9 @@ class ObjectSetViewModelFactory( objectStore = objectStore, addObjectToCollection = addObjectToCollection, objectToCollection = objectToCollection, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + getDefaultPageType = getDefaultPageType, + getTemplates = getTemplates ) as T } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/state/ObjectState.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/state/ObjectState.kt index cd4cd8ad81..bc5911c552 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/state/ObjectState.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/state/ObjectState.kt @@ -32,7 +32,7 @@ sealed class ObjectState { override val details: Map = emptyMap(), override val objectRestrictions: List = emptyList(), override val dataViewRestrictions: List = emptyList(), - override val objectRelationLinks: List = emptyList() + override val objectRelationLinks: List = emptyList(), ) : DataView() { override val isInitialized get() = blocks.any { it.content is DV } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/templates/TemplateSelectViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/templates/TemplateSelectViewModel.kt index ea38c83f8a..1b13ed76e7 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/templates/TemplateSelectViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/templates/TemplateSelectViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_utils.common.EventWrapper import com.anytypeio.anytype.domain.base.fold @@ -71,7 +72,16 @@ class TemplateSelectViewModel( layout = objType.recommendedLayout?.code ?: 0 ) ) - addAll(templates.map { TemplateView.Template(it.id) }) + addAll(templates.map { + TemplateView.Template( + id = it.id, + name = it.name.orEmpty(), + layout = it.layout ?: ObjectType.Layout.BASIC, + emoji = it.iconEmoji.orEmpty(), + image = it.iconImage.orEmpty(), + typeId = objType.id, + ) + }) } _viewState.emit( ViewState.Success( @@ -142,11 +152,4 @@ class TemplateSelectViewModel( object ErrorGettingType : ViewState() } - sealed class TemplateView { - data class Blank( - val typeId: Id, val typeName: String, val layout: Int - ) : TemplateView() - - data class Template(val id: Id) : TemplateView() - } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/templates/TemplateView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/templates/TemplateView.kt new file mode 100644 index 0000000000..bbc8c9c3f3 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/templates/TemplateView.kt @@ -0,0 +1,20 @@ +package com.anytypeio.anytype.presentation.templates + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType + +sealed class TemplateView { + + data class Blank( + val typeId: Id, val typeName: String = "", val layout: Int + ) : TemplateView() + + data class Template( + val id: Id, + val name: String, + val typeId: Id, + val layout: ObjectType.Layout, + val emoji: String?, + val image: String? + ) : TemplateView() +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/TemplateView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/TemplateView.kt new file mode 100644 index 0000000000..b2795ea2f8 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/TemplateView.kt @@ -0,0 +1,15 @@ +package com.anytypeio.anytype.presentation.widgets + +import com.anytypeio.anytype.presentation.templates.TemplateView + +data class TemplatesWidgetUiState( + val items: List, + val showWidget: Boolean +) { + companion object { + fun empty() = TemplatesWidgetUiState( + items = emptyList(), + showWidget = false + ) + } +} diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionAddRelationTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionAddRelationTest.kt index 6f477ab5ad..583ea7e013 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionAddRelationTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionAddRelationTest.kt @@ -50,6 +50,7 @@ class CollectionAddRelationTest : ObjectSetViewModelTestSetup() { MockitoAnnotations.openMocks(this) viewModel = givenViewModel() objectCollection = MockCollection(context = root) + stubGetDefaultPageType() } @After @@ -62,7 +63,6 @@ class CollectionAddRelationTest : ObjectSetViewModelTestSetup() { @Test fun `should add new relation to data view`() = runTest { // SETUP - stubWorkspaceManager(objectCollection.workspaceId) stubStoreOfRelations(objectCollection) stubSubscriptionResults( diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionDataViewUpdateTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionDataViewUpdateTest.kt index a5de3d3d4d..12b8964ed4 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionDataViewUpdateTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionDataViewUpdateTest.kt @@ -30,6 +30,7 @@ class CollectionDataViewUpdateTest : ObjectSetViewModelTestSetup() { MockitoAnnotations.openMocks(this) viewModel = givenViewModel() objectCollection = MockCollection(context = root) + stubGetDefaultPageType() } @After diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionTemplatesDelegateTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionTemplatesDelegateTest.kt new file mode 100644 index 0000000000..1b8e43bc97 --- /dev/null +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/CollectionTemplatesDelegateTest.kt @@ -0,0 +1,155 @@ +package com.anytypeio.anytype.presentation.collections + +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectTypeIds +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.domain.templates.GetTemplates +import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel +import com.anytypeio.anytype.presentation.sets.main.ObjectSetViewModelTestSetup +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions + +@OptIn(ExperimentalCoroutinesApi::class) +class CollectionTemplatesDelegateTest: ObjectSetViewModelTestSetup() { + + private lateinit var viewModel: ObjectSetViewModel + private lateinit var mockCollection: MockCollection + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + viewModel = givenViewModel() + } + + @After + fun after() { + rule.advanceTime() + } + + @Test + fun `should start get templates when collection with default type allowed templates`() = runTest { + + val defaultType = ObjectTypeIds.PAGE + val defaultTypeName = "Page" + val defaultTypeMap = mapOf( + Relations.ID to defaultType, + Relations.TYPE to ObjectTypeIds.OBJECT_TYPE, + Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.BASIC.code.toDouble(), + Relations.NAME to defaultTypeName + ) + mockCollection = MockCollection(context = root) + + // SETUP + stubWorkspaceManager(mockCollection.workspaceId) + stubInterceptEvents() + stubInterceptThreadStatus() + stubStoreOfObjectTypes(defaultTypeMap) + stubGetDefaultPageType(type = defaultType, name = defaultTypeName) + stubGetTemplates(type = defaultType) + + val details = Block.Details( + details = mapOf( + root to Block.Fields( + mapOf( + Relations.ID to root, + Relations.LAYOUT to ObjectType.Layout.COLLECTION.code.toDouble(), + Relations.TYPE to ObjectTypeIds.COLLECTION + ) + ) + ) + ) + + stubOpenObject( + doc = listOf(mockCollection.header, mockCollection.title, mockCollection.dataView), + details = details + ) + stubSubscriptionResults( + subscription = mockCollection.subscriptionId, + collection = root, + workspace = mockCollection.workspaceId, + storeOfRelations = storeOfRelations, + keys = mockCollection.dvKeys, + objects = listOf(mockCollection.obj1, mockCollection.obj2), + dvSorts = mockCollection.sorts + ) + + // TESTING + viewModel.onStart(ctx = root) + + advanceUntilIdle() + + verify(getTemplates, times(1)).async( + GetTemplates.Params(type = defaultType) + ) + + verifyNoInteractions(createDataViewObject) + } + + @Test + fun `should not start get templates when collection with default type not allowed templates`() = runTest { + + val defaultType = ObjectTypeIds.NOTE + val defaultTypeName = "Note" + val defaultTypeMap = mapOf( + Relations.ID to defaultType, + Relations.TYPE to ObjectTypeIds.OBJECT_TYPE, + Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.NOTE.code.toDouble(), + Relations.NAME to defaultTypeName + ) + mockCollection = MockCollection(context = root) + + // SETUP + stubWorkspaceManager(mockCollection.workspaceId) + stubInterceptEvents() + stubInterceptThreadStatus() + stubStoreOfObjectTypes(defaultTypeMap) + stubGetDefaultPageType(type = defaultType, name = defaultTypeName) + + val details = Block.Details( + details = mapOf( + root to Block.Fields( + mapOf( + Relations.ID to root, + Relations.LAYOUT to ObjectType.Layout.COLLECTION.code.toDouble(), + Relations.TYPE to ObjectTypeIds.COLLECTION + ) + ) + ) + ) + + stubOpenObject( + doc = listOf(mockCollection.header, mockCollection.title, mockCollection.dataView), + details = details + ) + stubSubscriptionResults( + subscription = mockCollection.subscriptionId, + collection = root, + workspace = mockCollection.workspaceId, + storeOfRelations = storeOfRelations, + keys = mockCollection.dvKeys, + objects = listOf(mockCollection.obj1, mockCollection.obj2), + dvSorts = mockCollection.sorts + ) + + // TESTING + viewModel.onStart(ctx = root) + + advanceUntilIdle() + + viewModel.onNewButtonIconClicked() + + advanceUntilIdle() + + verifyNoInteractions(getTemplates) + verifyNoInteractions(createDataViewObject) + } +} \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/MockSet.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/MockSet.kt index e5c3410970..b337200356 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/MockSet.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/MockSet.kt @@ -20,7 +20,7 @@ import com.anytypeio.anytype.presentation.sets.subscription.DefaultDataViewSubsc import com.anytypeio.anytype.test_utils.MockDataFactory import net.bytebuddy.utility.RandomString -class MockSet(context: String, setOfValue: String = "setOf-${RandomString.make()}") { +class MockSet(context: String, val setOfValue: String = "setOf-${RandomString.make()}") { val root = context val title = @@ -34,7 +34,7 @@ class MockSet(context: String, setOfValue: String = "setOf-${RandomString.make() ) val workspaceId = "workspace-${RandomString.make()}" val subscriptionId = DefaultDataViewSubscription.getSubscriptionId(context) - val setOf = setOfValue + val setOf get() = setOfValue val setOfNote = ObjectTypeIds.NOTE // RELATION OBJECTS @@ -180,7 +180,8 @@ class MockSet(context: String, setOfValue: String = "setOf-${RandomString.make() setOf to Block.Fields( map = mapOf( Relations.ID to setOf, - Relations.TYPE to ObjectTypeIds.OBJECT_TYPE + Relations.TYPE to ObjectTypeIds.OBJECT_TYPE, + Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.BASIC.code.toDouble(), ) ) ) diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectCreateTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectCreateTest.kt index 7680b0a196..0140011ef6 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectCreateTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectCreateTest.kt @@ -31,6 +31,7 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { fun setup() { MockitoAnnotations.openMocks(this) viewModel = givenViewModel() + stubGetDefaultPageType() } @After @@ -69,7 +70,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { doReturn(Resultat.success(result)).`when`(createDataViewObject).async( CreateDataViewObject.Params.SetByType( type = ObjectTypeIds.NOTE, - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) doReturn(Resultat.success(Unit)).`when`(closeBlock).async(mockObjectSet.root) @@ -79,7 +81,7 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { advanceUntilIdle() - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() advanceUntilIdle() @@ -87,7 +89,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { async( CreateDataViewObject.Params.SetByType( type = ObjectTypeIds.NOTE, - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) } @@ -125,7 +128,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { doReturn(Resultat.success(result)).`when`(createDataViewObject).async( CreateDataViewObject.Params.SetByType( type = ObjectTypeIds.PAGE, - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) doReturn(Resultat.success(Unit)).`when`(closeBlock).async(mockObjectSet.root) @@ -136,7 +140,7 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { advanceUntilIdle() - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() assertIs(commandFlow.awaitItem()) @@ -146,7 +150,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { async( CreateDataViewObject.Params.SetByType( type = ObjectTypeIds.PAGE, - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) } @@ -184,7 +189,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { doReturn(Resultat.success(result)).`when`(createDataViewObject).async( CreateDataViewObject.Params.SetByRelation( relations = listOf(mockObjectSet.relationObject3.id), - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) doReturn(Resultat.success(Unit)).`when`(closeBlock).async(mockObjectSet.root) @@ -194,7 +200,7 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { advanceUntilIdle() - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() advanceUntilIdle() @@ -202,7 +208,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { async( CreateDataViewObject.Params.SetByRelation( relations = listOf(mockObjectSet.relationObject3.id), - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) } @@ -244,7 +251,9 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { objectType = ObjectTypeIds.NOTE ) doReturn(Resultat.success(result)).`when`(createDataViewObject).async( - CreateDataViewObject.Params.Collection + CreateDataViewObject.Params.Collection( + templateId = null + ) ) doReturn(Resultat.success(Unit)).`when`(closeBlock).async(objectCollection.root) @@ -253,12 +262,12 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() { advanceUntilIdle() - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() advanceUntilIdle() verifyBlocking(createDataViewObject, times(1)) { - async(CreateDataViewObject.Params.Collection) + async(CreateDataViewObject.Params.Collection(null)) } verifyBlocking(closeBlock, times(1)) { async(objectCollection.root)} diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectStateCollectionViewTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectStateCollectionViewTest.kt index 110d76cb17..228778003d 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectStateCollectionViewTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectStateCollectionViewTest.kt @@ -1,14 +1,21 @@ package com.anytypeio.anytype.presentation.collections import app.cash.turbine.testIn +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectTypeIds +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.StubObject import com.anytypeio.anytype.presentation.sets.DataViewViewState import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel import com.anytypeio.anytype.presentation.sets.SetOrCollectionHeaderState import com.anytypeio.anytype.presentation.sets.main.ObjectSetViewModelTestSetup import com.anytypeio.anytype.presentation.sets.model.Viewer import com.anytypeio.anytype.presentation.sets.state.ObjectState +import com.anytypeio.anytype.test_utils.MockDataFactory import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertIs +import kotlin.test.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -29,6 +36,7 @@ class ObjectStateCollectionViewTest : ObjectSetViewModelTestSetup() { MockitoAnnotations.openMocks(this) viewModel = givenViewModel() mockObjectCollection = MockCollection(context = root) + stubGetDefaultPageType() } @After @@ -487,6 +495,7 @@ class ObjectStateCollectionViewTest : ObjectSetViewModelTestSetup() { ), dvSorts = listOf(mockObjectCollection.sortGallery) ) + stubGetTemplates() // TESTING viewModel.onStart(ctx = root) @@ -510,4 +519,167 @@ class ObjectStateCollectionViewTest : ObjectSetViewModelTestSetup() { assertEquals(mockObjectCollection.obj5.id, rows[4].objectId) } } + + @Test + fun `should be collection with templates present when default type is custom with proper recommended layout`() = runTest { + // SETUP + + val defaultObjectType = MockDataFactory.randomString() + val defaultObjectTypeName = "CustomName" + + stubWorkspaceManager(mockObjectCollection.workspaceId) + stubInterceptEvents() + stubInterceptThreadStatus() + stubGetDefaultPageType(defaultObjectType, defaultObjectTypeName) + stubGetTemplates( + type = defaultObjectType, + templates = listOf(StubObject(objectType = defaultObjectType)) + ) + + stubOpenObject( + doc = listOf( + mockObjectCollection.header, + mockObjectCollection.title, + mockObjectCollection.dataView + ), + details = mockObjectCollection.details + ) + stubStoreOfObjectTypes( + mapOf( + Relations.ID to defaultObjectType, + Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.BASIC.code.toDouble(), + Relations.NAME to defaultObjectTypeName + ) + ) + stubStoreOfRelations(mockObjectCollection) + stubSubscriptionResults( + subscription = mockObjectCollection.subscriptionId, + workspace = mockObjectCollection.workspaceId, + collection = root, + storeOfRelations = storeOfRelations, + keys = mockObjectCollection.dvKeys, + dvSorts = mockObjectCollection.sorts + ) + + // TESTING + viewModel.onStart(ctx = root) + + val viewerFlow = viewModel.currentViewer.testIn(backgroundScope) + val stateFlow = stateReducer.state.testIn(backgroundScope) + + // ASSERT STATES + assertIs(stateFlow.awaitItem()) + assertIs(viewerFlow.awaitItem()) + assertIs(stateFlow.awaitItem()) + + val item = viewerFlow.awaitItem() + assertIs(item) + assertTrue(item.hasTemplates) + } + + @Test + fun `should be collection without templates allowed when default type is custom with not proper recommended layout`() = runTest { + // SETUP + + val defaultObjectType = MockDataFactory.randomString() + val defaultObjectTypeName = "CustomName" + + stubWorkspaceManager(mockObjectCollection.workspaceId) + stubInterceptEvents() + stubInterceptThreadStatus() + stubGetDefaultPageType(defaultObjectType, defaultObjectTypeName) + + stubOpenObject( + doc = listOf( + mockObjectCollection.header, + mockObjectCollection.title, + mockObjectCollection.dataView + ), + details = mockObjectCollection.details + ) + stubStoreOfObjectTypes( + mapOf( + Relations.ID to defaultObjectType, + Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.SET.code.toDouble(), + Relations.NAME to defaultObjectTypeName + ) + ) + stubStoreOfRelations(mockObjectCollection) + stubSubscriptionResults( + subscription = mockObjectCollection.subscriptionId, + workspace = mockObjectCollection.workspaceId, + collection = root, + storeOfRelations = storeOfRelations, + keys = mockObjectCollection.dvKeys, + dvSorts = mockObjectCollection.sorts + ) + + // TESTING + viewModel.onStart(ctx = root) + + val viewerFlow = viewModel.currentViewer.testIn(backgroundScope) + val stateFlow = stateReducer.state.testIn(backgroundScope) + + // ASSERT STATES + assertIs(stateFlow.awaitItem()) + assertIs(viewerFlow.awaitItem()) + assertIs(stateFlow.awaitItem()) + + val item = viewerFlow.awaitItem() + assertIs(item) + assertFalse(item.hasTemplates) + } + + @Test + fun `should be collection without templates allowed when default type is NOTE`() = runTest { + // SETUP + + val defaultObjectType = ObjectTypeIds.NOTE + val defaultObjectTypeName = "Note" + + stubWorkspaceManager(mockObjectCollection.workspaceId) + stubInterceptEvents() + stubInterceptThreadStatus() + stubGetDefaultPageType(defaultObjectType, defaultObjectTypeName) + + stubOpenObject( + doc = listOf( + mockObjectCollection.header, + mockObjectCollection.title, + mockObjectCollection.dataView + ), + details = mockObjectCollection.details + ) + stubStoreOfObjectTypes( + mapOf( + Relations.ID to defaultObjectType, + Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.NOTE.code.toDouble(), + Relations.NAME to defaultObjectTypeName + ) + ) + stubStoreOfRelations(mockObjectCollection) + stubSubscriptionResults( + subscription = mockObjectCollection.subscriptionId, + workspace = mockObjectCollection.workspaceId, + collection = root, + storeOfRelations = storeOfRelations, + keys = mockObjectCollection.dvKeys, + dvSorts = mockObjectCollection.sorts + ) + + // TESTING + viewModel.onStart(ctx = root) + + val viewerFlow = viewModel.currentViewer.testIn(backgroundScope) + val stateFlow = stateReducer.state.testIn(backgroundScope) + + // ASSERT STATES + assertIs(stateFlow.awaitItem()) + assertIs(viewerFlow.awaitItem()) + assertIs(stateFlow.awaitItem()) + + val item = viewerFlow.awaitItem() + assertIs(item) + assertFalse(item.hasTemplates) + } } \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectStateSetViewTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectStateSetViewTest.kt index 142deaab1a..f1f8bde181 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectStateSetViewTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/collections/ObjectStateSetViewTest.kt @@ -7,14 +7,18 @@ import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeIds import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.StubObject import com.anytypeio.anytype.presentation.relations.ObjectSetConfig import com.anytypeio.anytype.presentation.search.ObjectSearchConstants import com.anytypeio.anytype.presentation.sets.DataViewViewState import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel import com.anytypeio.anytype.presentation.sets.main.ObjectSetViewModelTestSetup import com.anytypeio.anytype.presentation.sets.state.ObjectState +import com.anytypeio.anytype.test_utils.MockDataFactory import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertIs +import kotlin.test.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -408,4 +412,92 @@ class ObjectStateSetViewTest : ObjectSetViewModelTestSetup() { stateFlow.ensureAllEventsConsumed() viewerFlow.ensureAllEventsConsumed() } + + @Test + fun `displaying set with templates present when opening object set of pages with templates`() = runTest { + // SETUP + + mockObjectSet = MockSet(context = root, setOfValue = ObjectTypeIds.PAGE) + val pageTypeMap = mapOf( + Relations.ID to ObjectTypeIds.PAGE, + Relations.TYPE to ObjectTypeIds.OBJECT_TYPE, + Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.BASIC.code.toDouble(), + Relations.NAME to MockDataFactory.randomString() + ) + stubWorkspaceManager(mockObjectSet.workspaceId) + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenObject( + doc = listOf(mockObjectSet.header, mockObjectSet.title, mockObjectSet.dataView), + details = mockObjectSet.details + ) + stubSubscriptionResults( + subscription = mockObjectSet.subscriptionId, + workspace = mockObjectSet.workspaceId, + storeOfRelations = storeOfRelations, + keys = mockObjectSet.dvKeys, + sources = listOf(ObjectTypeIds.PAGE), + dvFilters = mockObjectSet.filters + ) + stubStoreOfObjectTypes(pageTypeMap) + stubGetTemplates( + type = ObjectTypeIds.PAGE, + templates = listOf(StubObject(objectType = ObjectTypeIds.PAGE) + ) + ) + + // TESTING + viewModel.onStart(ctx = root) + + // ASSERT STATES + val viewerFlow = viewModel.currentViewer.testIn(backgroundScope) + val stateFlow = stateReducer.state.testIn(backgroundScope) + + // ASSERT STATES + assertIs(stateFlow.awaitItem()) + assertIs(stateFlow.awaitItem()) + assertIs(viewerFlow.awaitItem()) + + val item = viewerFlow.awaitItem() + assertIs(item) + assertTrue(item.hasTemplates) + } + + @Test + fun `displaying set without templates allowed when opening object set of notes`() = runTest { + // SETUP + + mockObjectSet = MockSet(context = root, setOfValue = ObjectTypeIds.NOTE) + stubWorkspaceManager(mockObjectSet.workspaceId) + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenObject( + doc = listOf(mockObjectSet.header, mockObjectSet.title, mockObjectSet.dataView), + details = mockObjectSet.details + ) + stubSubscriptionResults( + subscription = mockObjectSet.subscriptionId, + workspace = mockObjectSet.workspaceId, + storeOfRelations = storeOfRelations, + keys = mockObjectSet.dvKeys, + sources = listOf(ObjectTypeIds.NOTE), + dvFilters = mockObjectSet.filters + ) + + // TESTING + viewModel.onStart(ctx = root) + + // ASSERT STATES + val viewerFlow = viewModel.currentViewer.testIn(backgroundScope) + val stateFlow = stateReducer.state.testIn(backgroundScope) + + // ASSERT STATES + assertIs(stateFlow.awaitItem()) + assertIs(stateFlow.awaitItem()) + assertIs(viewerFlow.awaitItem()) + + val item = viewerFlow.awaitItem() + assertIs(item) + assertFalse(item.hasTemplates) + } } \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/ObjectTypeExtensionsKtTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/ObjectTypeExtensionsKtTest.kt new file mode 100644 index 0000000000..b518fcfd79 --- /dev/null +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/ObjectTypeExtensionsKtTest.kt @@ -0,0 +1,81 @@ +package com.anytypeio.anytype.presentation.objects + +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectTypeIds +import com.anytypeio.anytype.core_models.StubObjectType +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test + +class ObjectTypeExtensionsTest { + + @Test + fun `isTemplateAllowed returns true when type is not in getNoTemplates and recommendedLayout is in editorLayouts`() { + val objectType = StubObjectType( + id = ObjectTypeIds.PAGE, + recommendedLayout = ObjectType.Layout.BASIC.code.toDouble() + ) + val result = objectType.isTemplatesAllowed() + assertTrue(result) + } + + @Test + fun `isTemplateAllowed returns false when type is not in getNoTemplates and recommendedLayout is not in editorLayouts`() { + val objectType = StubObjectType( + id = ObjectTypeIds.PAGE, + recommendedLayout = ObjectType.Layout.DASHBOARD.code.toDouble() + ) + val result = objectType.isTemplatesAllowed() + assertFalse(result) + } + + @Test + fun `isTemplateAllowed returns false when type is BOOKMARK`() { + val objectType = StubObjectType( + id = ObjectTypeIds.BOOKMARK, + recommendedLayout = ObjectType.Layout.BASIC.code.toDouble() + ) + val result = objectType.isTemplatesAllowed() + assertFalse(result) + } + + @Test + fun `isTemplateAllowed returns false when type is FILE`() { + val objectType = StubObjectType( + id = ObjectTypeIds.FILE, + recommendedLayout = ObjectType.Layout.BASIC.code.toDouble() + ) + val result = objectType.isTemplatesAllowed() + assertFalse(result) + } + + @Test + fun `isTemplateAllowed returns false when type is NOTE`() { + val objectType = StubObjectType( + id = ObjectTypeIds.NOTE, + recommendedLayout = ObjectType.Layout.NOTE.code.toDouble() + ) + val result = objectType.isTemplatesAllowed() + assertFalse(result) + } + + @Test + fun `isTemplateAllowed returns false when type is SET`() { + val objectType = StubObjectType( + id = ObjectTypeIds.SET, + recommendedLayout = ObjectType.Layout.SET.code.toDouble() + ) + val result = objectType.isTemplatesAllowed() + assertFalse(result) + } + + @Test + fun `isTemplateAllowed returns false when type is COLLECTION`() { + val objectType = StubObjectType( + id = ObjectTypeIds.COLLECTION, + recommendedLayout = ObjectType.Layout.COLLECTION.code.toDouble() + ) + val result = objectType.isTemplatesAllowed() + assertFalse(result) + } +} diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt index bbe391f5e9..6122334502 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt @@ -12,6 +12,7 @@ import com.anytypeio.anytype.core_models.StubFilter import com.anytypeio.anytype.core_models.StubRelationLink import com.anytypeio.anytype.core_models.StubSort import com.anytypeio.anytype.core_models.StubTitle +import com.anytypeio.anytype.domain.launch.GetDefaultPageType import com.anytypeio.anytype.presentation.sets.state.DefaultObjectStateReducer import com.anytypeio.anytype.presentation.sets.state.ObjectState import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetConvertToCollectionTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetConvertToCollectionTest.kt index b916cdec49..3fb380b128 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetConvertToCollectionTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetConvertToCollectionTest.kt @@ -32,6 +32,7 @@ class ObjectSetConvertToCollectionTest : ObjectSetViewModelTestSetup() { MockitoAnnotations.openMocks(this) viewModel = givenViewModel() mockObjectSet = MockSet(context = root) + stubGetDefaultPageType() } @After diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetDataViewObjectCreateTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetDataViewObjectCreateTest.kt index 1f56a11ce6..773c9d1824 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetDataViewObjectCreateTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetDataViewObjectCreateTest.kt @@ -57,7 +57,8 @@ class ObjectSetDataViewObjectCreateTest : ObjectSetViewModelTestSetup() { doReturn(Unit).`when`(createDataViewObject).async( CreateDataViewObject.Params.SetByType( type = mockObjectSet.setOf, - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) @@ -72,14 +73,15 @@ class ObjectSetDataViewObjectCreateTest : ObjectSetViewModelTestSetup() { val second = awaitItem() assertIs(second) - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() advanceUntilIdle() verifyBlocking(createDataViewObject, times(1)) { async( CreateDataViewObject.Params.SetByType( type = mockObjectSet.setOf, - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetInitializationTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetInitializationTest.kt index b427407821..f82c73572e 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetInitializationTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetInitializationTest.kt @@ -42,7 +42,7 @@ class ObjectSetInitializationTest : ObjectSetViewModelTestSetup() { // TESTING viewModel.onStart(ctx = root) - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() // ASSERT verifyNoInteractions(createObject) diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetRestrictionsTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetRestrictionsTest.kt index 0d3a5c3555..d449496fa3 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetRestrictionsTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetRestrictionsTest.kt @@ -118,7 +118,7 @@ class ObjectSetRestrictionsTest : ObjectSetViewModelTestSetup() { // ASSERT ERROR TOAST viewModel.toasts.test { - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() assertEquals(ObjectSetViewModel.NOT_ALLOWED, awaitItem()) cancelAndIgnoreRemainingEvents() } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt index 8f48793d00..215e5ac1a2 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt @@ -16,6 +16,7 @@ import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.base.Either import com.anytypeio.anytype.domain.base.Result +import com.anytypeio.anytype.domain.base.Resultat import com.anytypeio.anytype.domain.block.interactor.UpdateText import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.collections.AddObjectToCollection @@ -23,6 +24,7 @@ import com.anytypeio.anytype.domain.config.Gateway import com.anytypeio.anytype.domain.cover.SetDocCoverImage import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewObject import com.anytypeio.anytype.domain.event.interactor.InterceptEvents +import com.anytypeio.anytype.domain.launch.GetDefaultPageType import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.`object`.ConvertObjectToCollection import com.anytypeio.anytype.domain.`object`.UpdateDetail @@ -54,6 +56,7 @@ import com.anytypeio.anytype.presentation.sets.ObjectSetPaginator import com.anytypeio.anytype.presentation.sets.ObjectSetSession import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel import com.anytypeio.anytype.presentation.sets.state.DefaultObjectStateReducer +import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer import com.anytypeio.anytype.presentation.sets.subscription.DataViewSubscription import com.anytypeio.anytype.presentation.sets.subscription.DefaultDataViewSubscription import com.anytypeio.anytype.presentation.sets.updateFormatForSubscription @@ -147,6 +150,9 @@ open class ObjectSetViewModelTestSetup { @Mock lateinit var storeOfObjectTypes: StoreOfObjectTypes + @Mock + lateinit var getDefaultPageType: GetDefaultPageType + var stateReducer = DefaultObjectStateReducer() lateinit var dataViewSubscriptionContainer: DataViewSubscriptionContainer @@ -165,6 +171,9 @@ open class ObjectSetViewModelTestSetup { val urlBuilder: UrlBuilder get() = UrlBuilder(gateway) + val defaultObjectPageType = MockDataFactory.randomString() + val defaultObjectPageTypeName = MockDataFactory.randomString() + lateinit var dispatchers: AppCoroutineDispatchers fun givenViewModel(): ObjectSetViewModel { @@ -210,7 +219,9 @@ open class ObjectSetViewModelTestSetup { addObjectToCollection = addObjectToCollection, objectToCollection = objectToCollection, setQueryToObjectSet = setQueryToObjectSet, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + getDefaultPageType = getDefaultPageType, + getTemplates = getTemplates ) } @@ -262,25 +273,6 @@ open class ObjectSetViewModelTestSetup { } } - fun stubGetTemplates( - type: String, - templates: List = emptyList() - ) { - getTemplates.stub { - onBlocking { - run( - GetTemplates.Params(type) - ) - } doReturn templates.map { - ObjectWrapper.Basic( - map = mapOf( - Relations.ID to it - ) - ) - } - } - } - suspend fun stubWorkspaceManager(workspace: Id) { workspaceManager.setCurrentWorkspace(workspace) } @@ -353,9 +345,27 @@ open class ObjectSetViewModelTestSetup { ) } - fun stubStoreOfObjectTypes() { + fun stubStoreOfObjectTypes(map: Map = emptyMap()) { storeOfObjectTypes.stub { - onBlocking { get(any()) } doReturn ObjectWrapper.Type(map = emptyMap()) + onBlocking { get(any()) } doReturn ObjectWrapper.Type(map = map) + } + } + + fun stubGetDefaultPageType(type: String = defaultObjectPageType, name: String = defaultObjectPageTypeName) { + getDefaultPageType.stub { + onBlocking { run(Unit) } doReturn GetDefaultPageType.Response(type = type, name = name) + } + } + + fun stubGetTemplates( + type: String = MockDataFactory.randomString(), + templates: List = emptyList() + ) { + val params = GetTemplates.Params( + type = type + ) + getTemplates.stub { + onBlocking { async(params) }.thenReturn(Resultat.success(templates)) } } } \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/SetByRelationTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/SetByRelationTest.kt index 3731d62a7a..33541f3195 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/SetByRelationTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/SetByRelationTest.kt @@ -48,7 +48,6 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() { doc = listOf(mockObjectSet.header, mockObjectSet.title, mockObjectSet.dataView), details = mockObjectSet.details ) - stubGetTemplates(type = mockObjectSet.setOf) stubSubscriptionResults( subscription = mockObjectSet.subscriptionId, workspace = mockObjectSet.workspaceId, @@ -61,7 +60,8 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() { doReturn(Unit).`when`(createDataViewObject).async( CreateDataViewObject.Params.SetByType( type = mockObjectSet.setOf, - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) @@ -76,14 +76,15 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() { val second = awaitItem() assertIs(second) - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() advanceUntilIdle() verifyBlocking(createDataViewObject, times(1)) { async( CreateDataViewObject.Params.SetByType( type = mockObjectSet.setOf, - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) } @@ -101,7 +102,6 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() { doc = listOf(mockObjectSet.header, mockObjectSet.title, mockObjectSet.dataView), details = mockObjectSet.detailsSetByRelation ) - stubGetTemplates(type = mockObjectSet.setOf) stubSubscriptionResults( subscription = mockObjectSet.subscriptionId, workspace = mockObjectSet.workspaceId, @@ -114,7 +114,8 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() { doReturn(Unit).`when`(createDataViewObject).async( CreateDataViewObject.Params.SetByRelation( relations = listOf(mockObjectSet.relationObject3.id), - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) @@ -129,14 +130,15 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() { val second = awaitItem() assertIs(second) - viewModel.onCreateNewDataViewObject() + viewModel.proceedWithCreatingNewDataViewObject() advanceUntilIdle() verifyBlocking(createDataViewObject, times(1)) { async( CreateDataViewObject.Params.SetByRelation( relations = listOf(mockObjectSet.relationObject3.id), - filters = mockObjectSet.filters + filters = mockObjectSet.filters, + template = null ) ) } diff --git a/sample/src/main/res/layout/fragment_buttons.xml b/sample/src/main/res/layout/fragment_buttons.xml index 514003f232..37cedb2d68 100644 --- a/sample/src/main/res/layout/fragment_buttons.xml +++ b/sample/src/main/res/layout/fragment_buttons.xml @@ -37,6 +37,27 @@ app:layout_constraintStart_toStartOf="@+id/textView21" app:layout_constraintTop_toBottomOf="@+id/button8" /> + + + + + app:layout_constraintTop_toBottomOf="@+id/btnSmallArrow" /> + app:layout_constraintTop_toBottomOf="@+id/btnCompose" /> \ No newline at end of file diff --git a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Object.kt b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Object.kt index 9bdf4530b6..ba6e31a2bc 100644 --- a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Object.kt +++ b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Object.kt @@ -62,7 +62,8 @@ fun StubObjectType( iconEmoji: String? = null, isReadOnly: Boolean? = null, isHidden: Boolean? = null, - sourceObject: Id? = null + sourceObject: Id? = null, + recommendedLayout: Double? = null ): ObjectWrapper.Type = ObjectWrapper.Type( map = mapOf( Relations.ID to id, @@ -76,6 +77,7 @@ fun StubObjectType( Relations.ICON_EMOJI to iconEmoji, Relations.IS_READ_ONLY to isReadOnly, Relations.IS_HIDDEN to isHidden, - Relations.SOURCE_OBJECT to sourceObject + Relations.SOURCE_OBJECT to sourceObject, + Relations.RECOMMENDED_LAYOUT to recommendedLayout ) ) \ No newline at end of file