1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +09:00

DROID-3429 Primitives | Edit type properties, part 1 (#2138)

This commit is contained in:
Konstantin Ivanov 2025-03-10 13:46:53 +01:00 committed by GitHub
parent b566ba30e0
commit 3932c2fe87
Signed by: github
GPG key ID: B5690EEEBB952194
42 changed files with 750 additions and 1142 deletions

View file

@ -177,6 +177,7 @@ dependencies {
implementation project(':feature-all-content')
implementation project(':feature-date')
implementation project(':feature-object-type')
implementation project(':feature-properties')
//Compile time dependencies
ksp libs.daggerCompiler

View file

@ -9,6 +9,7 @@ import com.anytypeio.anytype.di.feature.DaggerAllContentComponent
import com.anytypeio.anytype.di.feature.DaggerAppPreferencesComponent
import com.anytypeio.anytype.di.feature.DaggerBacklinkOrAddToObjectComponent
import com.anytypeio.anytype.di.feature.DaggerDateObjectComponent
import com.anytypeio.anytype.di.feature.DaggerEditTypePropertiesComponent
import com.anytypeio.anytype.di.feature.DaggerLinkToObjectComponent
import com.anytypeio.anytype.di.feature.DaggerMoveToComponent
import com.anytypeio.anytype.di.feature.DaggerObjectTypeComponent
@ -109,6 +110,7 @@ import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams
import com.anytypeio.anytype.feature_chats.presentation.SelectChatReactionViewModel
import com.anytypeio.anytype.feature_date.viewmodel.DateObjectVmParams
import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel
@ -1141,6 +1143,12 @@ class ComponentManager(
.create(params, findComponentDependencies())
}
val editTypePropertiesComponent = ComponentWithParams { params: EditTypePropertiesVmParams ->
DaggerEditTypePropertiesComponent
.factory()
.create(params, findComponentDependencies())
}
class Component<T>(private val builder: () -> T) {
private var instance: T? = null

View file

@ -5,14 +5,12 @@ import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.debugging.Logger
import com.anytypeio.anytype.domain.event.interactor.EventChannel
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
import com.anytypeio.anytype.domain.launch.GetDefaultObjectType
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
@ -22,15 +20,14 @@ import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.DeleteObjects
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields
import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields
import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams
import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.providers.DefaultCoverImageHashProvider
@ -81,19 +78,6 @@ object ObjectTypeModule {
logger = logger
)
@JvmStatic
@Provides
@PerScreen
fun createObject(
repo: BlockRepository,
getDefaultObjectType: GetDefaultObjectType,
dispatchers: AppCoroutineDispatchers,
): CreateObject = CreateObject(
repo = repo,
getDefaultObjectType = getDefaultObjectType,
dispatchers = dispatchers
)
@JvmStatic
@Provides
@PerScreen
@ -123,13 +107,6 @@ object ObjectTypeModule {
dispatchers: AppCoroutineDispatchers
): GetObjectTypeConflictingFields = GetObjectTypeConflictingFields(repo, dispatchers)
@JvmStatic
@Provides
@PerScreen
fun provideCreateObjectSetUseCase(
repo: BlockRepository
): CreateObjectSet = CreateObjectSet(repo = repo)
@JvmStatic
@Provides
@PerScreen

View file

@ -0,0 +1,88 @@
package com.anytypeio.anytype.di.feature
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.core_utils.di.scope.PerModal
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields
import com.anytypeio.anytype.domain.relations.CreateRelation
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModelFactory
import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams
import com.anytypeio.anytype.ui.primitives.EditTypePropertiesFragment
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
//region EDIT OBJECT TYPE PROPERTIES SCREEN
@PerModal
@Component(
modules = [
EditTypePropertiesModule::class,
EditTypePropertiesModule.Declarations::class
],
dependencies = [EditTypePropertiesDependencies::class]
)
interface EditTypePropertiesComponent {
@Component.Factory
interface Factory {
fun create(
@BindsInstance vmParams: EditTypePropertiesVmParams,
dependencies: EditTypePropertiesDependencies
): EditTypePropertiesComponent
}
fun inject(fragment: EditTypePropertiesFragment)
}
@Module
object EditTypePropertiesModule {
@JvmStatic
@Provides
@PerModal
fun provideTypeSetRecommendedFields(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
): SetObjectTypeRecommendedFields = SetObjectTypeRecommendedFields(repo, dispatchers)
@JvmStatic
@Provides
@PerModal
fun createRelation(
repo: BlockRepository,
storeOfRelations: StoreOfRelations,
) = CreateRelation(repo, storeOfRelations)
@JvmStatic
@Provides
@PerModal
fun provideSetObjectDetails(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
): SetObjectDetails = SetObjectDetails(repo, dispatchers)
@Module
interface Declarations {
@PerModal
@Binds
fun bindViewModelFactory(
factory: EditTypePropertiesViewModelFactory
): ViewModelProvider.Factory
}
}
interface EditTypePropertiesDependencies : ComponentDependencies {
fun provideStringResourceProvider(): StringResourceProvider
fun provideStoreOfRelations(): StoreOfRelations
fun provideStoreOfObjectTypes(): StoreOfObjectTypes
fun provideBlockRepository(): BlockRepository
fun provideAppCoroutineDispatchers(): AppCoroutineDispatchers
}
//endregion

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.di.main
import com.anytypeio.anytype.app.AndroidApplication
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.di.common.ComponentDependenciesKey
import com.anytypeio.anytype.di.feature.EditTypePropertiesDependencies
import com.anytypeio.anytype.di.feature.AllContentDependencies
import com.anytypeio.anytype.di.feature.AppPreferencesDependencies
import com.anytypeio.anytype.di.feature.BacklinkOrAddToObjectDependencies
@ -143,6 +144,7 @@ interface MainComponent :
SelectChatReactionDependencies,
ChatReactionDependencies,
ParticipantComponentDependencies,
EditTypePropertiesDependencies,
DebugDependencies
{
@ -414,4 +416,9 @@ abstract class ComponentDependenciesModule {
@IntoMap
@ComponentDependenciesKey(DebugDependencies::class)
abstract fun provideDebugDependencies(component: MainComponent): ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(EditTypePropertiesDependencies::class)
abstract fun provideEditTypePropertiesDependencies(component: MainComponent): ComponentDependencies
}

View file

@ -0,0 +1,64 @@
package com.anytypeio.anytype.ui.primitives
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.fragment.compose.content
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModel
import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModelFactory
import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams
import javax.inject.Inject
class EditTypePropertiesFragment : BaseBottomSheetComposeFragment() {
@Inject
lateinit var viewModelFactory: EditTypePropertiesViewModelFactory
private val vm by viewModels<EditTypePropertiesViewModel> { viewModelFactory }
private val space get() = argString(ARG_SPACE)
private val typeId get() = argString(ARG_OBJECT_ID)
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = content {
MaterialTheme {
}
}
override fun injectDependencies() {
val params = EditTypePropertiesVmParams(
objectTypeId = typeId,
spaceId = SpaceId(space)
)
componentManager().editTypePropertiesComponent.get(params).inject(this)
}
override fun releaseDependencies() {
componentManager().editTypePropertiesComponent.release()
}
companion object {
fun args(objectId: Id, space: Id) = bundleOf(
ARG_OBJECT_ID to objectId,
ARG_SPACE to space
)
const val ARG_OBJECT_ID = "arg.primitives.edit.type.property.object.id"
const val ARG_SPACE = "arg.primitives.edit.type.property.space"
const val DEFAULT_PADDING_TOP = 10
}
}

View file

@ -302,11 +302,11 @@ class ObjectFieldsFragment : BaseBottomSheetComposeFragment(),
)
}
const val ARG_CTX = "arg.document-relation.ctx"
const val ARG_SPACE = "arg.document-relation.space"
const val ARG_TARGET = "arg.document-relation.target"
const val ARG_LOCKED = "arg.document-relation.locked"
const val ARG_SET_FLOW = "arg.document-relation.set-flow"
const val ARG_CTX = "arg.primitives.properties.ctx"
const val ARG_SPACE = "arg.primitives.properties.space"
const val ARG_TARGET = "arg.primitives.properties.target"
const val ARG_LOCKED = "arg.primitives.properties.locked"
const val ARG_SET_FLOW = "arg.primitives.properties.set_flow"
const val DEFAULT_PADDING_TOP = 10
}

View file

@ -44,9 +44,8 @@ class ObjectTypeFieldsFragment : BaseBottomSheetComposeFragment() {
uiFieldsListState = vm.uiFieldsListState.collectAsStateWithLifecycle().value,
uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value,
uiIconState = vm.uiIconState.collectAsStateWithLifecycle().value,
uiFieldEditOrNewState = vm.uiFieldEditOrNewState.collectAsStateWithLifecycle().value,
uiEditPropertyState = vm.uiEditPropertyScreen.collectAsStateWithLifecycle().value,
uiFieldLocalInfoState = vm.uiFieldLocalInfoState.collectAsStateWithLifecycle().value,
uiAddFieldsScreenState = vm.uiAddFieldsState.collectAsStateWithLifecycle().value,
fieldEvent = vm::onFieldEvent
)
}

View file

@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResultListener
@ -25,7 +24,6 @@ import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_ui.views.BaseAlertDialog
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.feature_object_type.fields.ui.FieldsMainScreen
@ -34,14 +32,7 @@ import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams
import com.anytypeio.anytype.feature_object_type.ui.UiErrorState
import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory
import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeViewModel
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.ui.chats.ChatFragment
import com.anytypeio.anytype.ui.date.DateObjectFragment
import com.anytypeio.anytype.ui.editor.EditorFragment
import com.anytypeio.anytype.ui.editor.EditorModalFragment
import com.anytypeio.anytype.ui.profile.ParticipantFragment
import com.anytypeio.anytype.ui.relations.RelationAddToObjectFragment
import com.anytypeio.anytype.ui.sets.ObjectSetFragment
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.TYPE_TEMPLATE_EDIT
import com.anytypeio.anytype.ui.types.picker.REQUEST_KEY_PICK_EMOJI
import com.anytypeio.anytype.ui.types.picker.REQUEST_KEY_REMOVE_EMOJI
@ -55,7 +46,6 @@ import timber.log.Timber
class ObjectTypeFragment : BaseComposeFragment() {
@Inject
lateinit var factory: ObjectTypeVMFactory
private val vm by viewModels<ObjectTypeViewModel> { factory }
private lateinit var navComposeController: NavHostController
@ -121,12 +111,18 @@ class ObjectTypeFragment : BaseComposeFragment() {
navComposeController.navigate(OBJ_TYPE_FIELDS)
}
is ObjectTypeCommand.OpenAddFieldScreen -> {
RelationAddToObjectFragment.new(
ctx = command.typeId,
space = command.space,
isSetOrCollection = command.isSet
).showChildFragment()
is ObjectTypeCommand.OpenEditTypePropertiesScreen -> {
runCatching {
findNavController().navigate(
R.id.editTypePropertiesScreen,
EditTypePropertiesFragment.args(
objectId = command.typeId,
space = command.space
)
)
}.onFailure {
Timber.e(it, "Error while opening edit object type properties screen")
}
}
}
}
@ -174,88 +170,12 @@ class ObjectTypeFragment : BaseComposeFragment() {
uiFieldsListState = vm.uiFieldsListState.collectAsStateWithLifecycle().value,
uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value,
uiIconState = vm.uiIconState.collectAsStateWithLifecycle().value,
uiFieldEditOrNewState = vm.uiFieldEditOrNewState.collectAsStateWithLifecycle().value,
uiEditPropertyState = vm.uiEditPropertyScreen.collectAsStateWithLifecycle().value,
uiFieldLocalInfoState = vm.uiFieldLocalInfoState.collectAsStateWithLifecycle().value,
uiAddFieldsScreenState = vm.uiAddFieldsState.collectAsStateWithLifecycle().value,
fieldEvent = vm::onFieldEvent
)
}
}
LaunchedEffect(Unit) {
vm.navigation.collect { nav ->
when (nav) {
is OpenObjectNavigation.OpenEditor -> {
findNavController().navigate(
R.id.objectNavigation,
EditorFragment.args(
ctx = nav.target,
space = nav.space
)
)
}
is OpenObjectNavigation.OpenDataView -> {
findNavController().navigate(
R.id.dataViewNavigation,
ObjectSetFragment.args(
ctx = nav.target,
space = nav.space
)
)
}
is OpenObjectNavigation.OpenParticipant -> {
runCatching {
findNavController().navigate(
R.id.participantScreen,
ParticipantFragment.args(
objectId = nav.target,
space = nav.space
)
)
}.onFailure {
Timber.w("Error while opening participant screen")
}
}
is OpenObjectNavigation.OpenChat -> {
findNavController().navigate(
R.id.chatScreen,
ChatFragment.args(
ctx = nav.target,
space = nav.space
)
)
}
OpenObjectNavigation.NonValidObject -> {
toast(getString(R.string.error_non_valid_object))
}
is OpenObjectNavigation.OpenDateObject -> {
runCatching {
findNavController().navigate(
R.id.dateObjectScreen,
DateObjectFragment.args(
objectId = nav.target,
space = nav.space
)
)
}.onFailure {
Timber.e(it, "Failed to navigate to date object screen")
}
}
is OpenObjectNavigation.UnexpectedLayoutError -> {
toast(getString(R.string.error_unexpected_layout))
}
else -> {
// Do nothing.
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@ -282,15 +202,6 @@ class ObjectTypeFragment : BaseComposeFragment() {
}
}
}
when (val state = errorStateScreen) {
UiErrorState.Hidden -> {
}
is UiErrorState.Show -> {
}
}
}
override fun injectDependencies() {

View file

@ -249,6 +249,10 @@
app:destination="@id/typeSetIconPickerScreen" />
</fragment>
<dialog
android:id="@+id/editTypePropertiesScreen"
android:name="com.anytypeio.anytype.ui.primitives.EditTypePropertiesFragment"/>
<dialog
android:id="@+id/selectWidgetSourceScreen"
android:name="com.anytypeio.anytype.ui.widgets.SelectWidgetSourceFragment" />

View file

@ -5,10 +5,42 @@ import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.R
@Composable
fun keyboardAsState(): State<Boolean> {
val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
return rememberUpdatedState(isImeVisible)
}
}
@Composable
fun Modifier.bottomBorder(
strokeWidth: Dp = 0.5.dp,
color: Color = colorResource(R.color.shape_primary)
) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height - strokeWidthPx / 2
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
}
}
)

View file

@ -278,8 +278,7 @@ fun ObjectLayoutView.getName(): Int? = when (this) {
@StringRes
fun RelationFormat.getPrettyName(): Int = when (this) {
RelationFormat.LONG_TEXT -> R.string.relation_format_long_text
RelationFormat.SHORT_TEXT -> R.string.relation_format_short_text
RelationFormat.LONG_TEXT, RelationFormat.SHORT_TEXT -> R.string.relation_format_long_text
RelationFormat.NUMBER -> R.string.relation_format_number
RelationFormat.STATUS -> R.string.relation_format_status
RelationFormat.TAG -> R.string.relation_format_tag

View file

@ -48,6 +48,7 @@ import com.anytypeio.anytype.core_ui.R
@Composable
fun DefaultSearchBar(
modifier: Modifier = Modifier,
hint: Int = R.string.search,
onQueryChanged: (String) -> Unit
) {
@ -80,7 +81,7 @@ fun DefaultSearchBar(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(
start = 11.dp
start = 10.dp
)
)
CompositionLocalProvider(value = LocalTextSelectionColors provides selectionColors) {
@ -117,9 +118,9 @@ fun DefaultSearchBar(
interactionSource = interactionSource,
placeholder = {
Text(
text = stringResource(id = R.string.search),
text = stringResource(id = hint),
style = BodyRegular.copy(
color = colorResource(id = R.color.text_tertiary)
color = colorResource(id = R.color.glyph_active)
)
)
},

View file

@ -209,18 +209,30 @@ class FieldParserImpl @Inject constructor(
storeOfRelations: StoreOfRelations
): ParsedFields {
val headerFields = storeOfRelations.getValidRelations(
ids = objType.recommendedFeaturedRelations
)
val sidebarFields = storeOfRelations.getValidRelations(
ids = objType.recommendedRelations
)
val hiddenFields = storeOfRelations.getValidRelations(
ids = objType.recommendedHiddenRelations
)
val fileFields = storeOfRelations.getValidRelations(
ids = objType.recommendedFileRelations
)
// Clean recommended IDs based on priority.
// recommendedFeaturedRelations always remain.
val featuredIds = objType.recommendedFeaturedRelations.distinct()
// recommendedRelations: remove any ids that appear in featuredIds.
val relationsIds = objType.recommendedRelations
.filter { it !in featuredIds }
.distinct()
// recommendedFileRelations: remove ids that are in featuredIds or relationsIds.
val fileIds = objType.recommendedFileRelations
.filter { it !in featuredIds && it !in relationsIds }
.distinct()
// recommendedHiddenRelations: remove ids that are in featuredIds, relationsIds, or fileIds.
val hiddenIds = objType.recommendedHiddenRelations
.filter { it !in featuredIds && it !in relationsIds && it !in fileIds }
.distinct()
// Fetch valid relations for each recommended group.
val headerFields = storeOfRelations.getValidRelations(ids = featuredIds)
val sidebarFields = storeOfRelations.getValidRelations(ids = relationsIds)
val fileFields = storeOfRelations.getValidRelations(ids = fileIds)
val hiddenFields = storeOfRelations.getValidRelations(ids = hiddenIds)
// Combine IDs from all recommended relations.
val existingIds = (headerFields + sidebarFields + hiddenFields + fileFields)

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.domain.resources
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.RelativeDate
interface StringResourceProvider {
@ -7,4 +8,5 @@ interface StringResourceProvider {
fun getDeletedObjectTitle(): String
fun getUntitledObjectTitle(): String
fun getSetOfObjectsTitle(): String
fun getPropertiesFormatPrettyString(format: RelationFormat): String
}

View file

@ -52,10 +52,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
@ -63,11 +59,11 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.common.bottomBorder
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
import com.anytypeio.anytype.core_ui.extensions.swapList
import com.anytypeio.anytype.core_ui.foundation.DefaultSearchBar
@ -89,7 +85,6 @@ import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
import com.anytypeio.anytype.feature_allcontent.BuildConfig
import com.anytypeio.anytype.feature_allcontent.R
import com.anytypeio.anytype.feature_allcontent.models.AllContentBottomMenu
import com.anytypeio.anytype.feature_allcontent.models.AllContentMenuMode
import com.anytypeio.anytype.feature_allcontent.models.AllContentTab
import com.anytypeio.anytype.feature_allcontent.models.UiContentItem
@ -824,29 +819,6 @@ object AllContentNavigation {
const val ALL_CONTENT_MAIN = "all_content_main"
}
@Composable
fun Modifier.bottomBorder(
strokeWidth: Dp = 0.5.dp,
color: Color = colorResource(R.color.shape_primary)
) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height - strokeWidthPx / 2
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
}
}
)
@Composable
fun SwipeToDismissListItems(
item: UiContentItem.Item,

View file

@ -28,6 +28,7 @@ dependencies {
implementation project(':localization')
implementation project(':presentation')
implementation project(':library-emojifier')
implementation project(':feature-properties')
compileOnly libs.javaxInject

View file

@ -1,35 +1,13 @@
package com.anytypeio.anytype.feature_object_type.fields
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.RelationFormat
sealed class FieldEvent {
data object OnFieldEditScreenDismiss : FieldEvent()
data object OnAddFieldScreenDismiss : FieldEvent()
data object OnEditPropertyScreenDismiss : FieldEvent()
data class OnFieldItemClick(val item: UiFieldsListItem) : FieldEvent()
data class OnAddToHeaderFieldClick(
val item: UiAddFieldItem
) : FieldEvent()
data class OnAddToSidebarFieldClick(
val item: UiAddFieldItem
) : FieldEvent()
data class OnSaveButtonClicked(
val name: String,
val format: RelationFormat,
val limitObjectTypes: List<Id>
) : FieldEvent()
data object OnChangeTypeClick : FieldEvent()
data object OnLimitTypesClick : FieldEvent()
sealed class FieldItemMenu : FieldEvent() {
data class OnDeleteFromTypeClick(val item: UiFieldsListItem) : FieldItemMenu()
data class OnRemoveLocalClick(val item: UiFieldsListItem) : FieldItemMenu()
data class OnAddLocalToTypeClick(val item: UiFieldsListItem) : FieldItemMenu()
}
@ -38,7 +16,6 @@ sealed class FieldEvent {
}
sealed class Section : FieldEvent() {
data object OnAddToHeaderIconClick : Section()
data object OnAddToSidebarIconClick : Section()
data object OnLocalInfoClick : Section()
}
@ -47,6 +24,4 @@ sealed class FieldEvent {
data class OnMove(val fromKey: String, val toKey: String) : DragEvent()
data object OnDragEnd : DragEvent()
}
data class OnAddFieldSearchQueryChanged(val query: String) : FieldEvent()
}

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.feature_object_type.fields
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.feature_properties.edit.UiPropertyLimitTypeItem
import com.anytypeio.anytype.presentation.objects.ObjectIcon
//region Top bar
@ -34,6 +35,7 @@ data class UiFieldsListState(val items: List<UiFieldsListItem>) {
}
}
//todo rename to UiPropertiesListItem
sealed class UiFieldsListItem {
abstract val id: Id
@ -41,7 +43,7 @@ sealed class UiFieldsListItem {
abstract val fieldKey: Key
abstract val fieldTitle: String
abstract val format: RelationFormat
abstract val limitObjectTypes: List<UiFieldObjectItem>
abstract val limitObjectTypes: List<UiPropertyLimitTypeItem>
abstract val canDelete: Boolean
abstract val isEditableField: Boolean
@ -50,7 +52,7 @@ sealed class UiFieldsListItem {
override val fieldKey: Key,
override val fieldTitle: String,
override val format: RelationFormat,
override val limitObjectTypes: List<UiFieldObjectItem> = emptyList(),
override val limitObjectTypes: List<UiPropertyLimitTypeItem> = emptyList(),
override val canDelete: Boolean,
override val isEditableField: Boolean
) : Item()
@ -60,7 +62,7 @@ sealed class UiFieldsListItem {
override val fieldKey: Key,
override val fieldTitle: String,
override val format: RelationFormat,
override val limitObjectTypes: List<UiFieldObjectItem> = emptyList(),
override val limitObjectTypes: List<UiPropertyLimitTypeItem> = emptyList(),
override val canDelete: Boolean = false,
override val isEditableField: Boolean
) : Item()
@ -186,18 +188,4 @@ sealed class UiLocalsFieldsInfoState {
}
//endregion
//region Add Fields screen
sealed class UiAddFieldsScreenState {
data object Hidden : UiAddFieldsScreenState()
data class Visible(val items: List<UiAddFieldItem>, val addToHeader: Boolean) : UiAddFieldsScreenState()
}
data class UiAddFieldItem(
val id: Id,
val fieldKey: Key,
val fieldTitle: String,
val format: RelationFormat
)
//endregion

View file

@ -1,170 +0,0 @@
package com.anytypeio.anytype.feature_object_type.fields.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
import com.anytypeio.anytype.core_ui.foundation.DefaultSearchBar
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddFieldScreen(
state: UiAddFieldsScreenState,
fieldEvent: (FieldEvent) -> Unit
) {
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
var isSearchEmpty by remember { mutableStateOf(true) }
val lazyListState = rememberLazyListState()
if (state is UiAddFieldsScreenState.Visible) {
ModalBottomSheet(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing)
.nestedScroll(rememberNestedScrollInteropConnection()),
dragHandle = { DragHandle() },
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
containerColor = colorResource(id = R.color.background_primary),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
sheetState = bottomSheetState,
onDismissRequest = {
fieldEvent(FieldEvent.OnAddFieldScreenDismiss)
},
) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
state = lazyListState
) {
item {
DefaultSearchBar(
modifier = Modifier.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
isSearchEmpty = it.isEmpty()
fieldEvent(FieldEvent.OnAddFieldSearchQueryChanged(it))
}
}
items(
count = state.items.size,
key = { index -> state.items[index].id },
itemContent = { index ->
val item = state.items[index]
FieldItem(
modifier = commonItemModifier()
.noRippleThrottledClickable {
if (state.addToHeader) {
fieldEvent(
FieldEvent.OnAddToHeaderFieldClick(
item = item
)
)
} else {
fieldEvent(
FieldEvent.OnAddToSidebarFieldClick(
item = item
)
)
}
},
item = item
)
}
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FieldItem(
modifier: Modifier,
item: UiAddFieldItem
) {
Row(
modifier = modifier,
verticalAlignment = CenterVertically
) {
val formatIcon = item.format.simpleIcon()
if (formatIcon != null) {
Image(
modifier = Modifier
.padding(end = 10.dp)
.size(24.dp),
painter = painterResource(id = formatIcon),
contentDescription = "Relation format icon",
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.weight(1.0f)
.padding(end = 16.dp),
text = item.fieldTitle,
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@DefaultPreviews
@Composable
fun PreviewAddFieldScreen() {
AddFieldScreen(
state = UiAddFieldsScreenState.Visible(
items = listOf(
UiAddFieldItem(
id = "1",
fieldKey = "key",
fieldTitle = "Title",
format = RelationFormat.LONG_TEXT
)
),
addToHeader = true
),
fieldEvent = {}
)
}

View file

@ -1,386 +0,0 @@
package com.anytypeio.anytype.feature_object_type.fields.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.extensions.getPrettyName
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
import com.anytypeio.anytype.feature_object_type.ui.createDummyFieldDraggableItem
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditFieldScreen(
modifier: Modifier,
uiFieldEditOrNewState: UiFieldEditOrNewState,
fieldEvent: (FieldEvent) -> Unit
) {
if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) {
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
modifier = modifier,
dragHandle = { DragHandle() },
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
containerColor = colorResource(id = R.color.background_primary),
shape = RoundedCornerShape(16.dp),
sheetState = bottomSheetState,
onDismissRequest = { fieldEvent(FieldEvent.OnFieldEditScreenDismiss) },
) {
EditFieldContent(
modifier = Modifier.fillMaxWidth(),
uiState = uiFieldEditOrNewState,
fieldEvent = fieldEvent
)
}
}
}
@Composable
private fun EditFieldContent(
modifier: Modifier,
uiState: UiFieldEditOrNewState.Visible,
fieldEvent: (FieldEvent) -> Unit
) {
val field = uiState.item
var innerValue by remember(field.fieldTitle) { mutableStateOf(field.fieldTitle) }
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val isEditable = field.isEditableField
val title = when (uiState) {
is UiFieldEditOrNewState.Visible.Edit -> stringResource(R.string.object_type_fields_edit_field)
is UiFieldEditOrNewState.Visible.New -> stringResource(R.string.object_type_fields_new_field)
is UiFieldEditOrNewState.Visible.ViewOnly -> stringResource(R.string.object_type_fields_preview_field)
}
Column(modifier = modifier) {
// Header title
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp),
contentAlignment = Alignment.Center
) {
Text(
text = title,
style = Title2,
color = colorResource(id = R.color.text_primary)
)
}
// Name text field
NameTextField(
modifier = Modifier.fillMaxWidth(),
value = innerValue,
isEditable = isEditable,
focusRequester = focusRequester,
keyboardController = keyboardController,
onValueChange = { innerValue = it }
)
Spacer(modifier = Modifier.height(10.dp))
Divider()
// Field type section
FieldTypeSection(
format = field.format,
isEditable = isEditable,
onTypeClick = { fieldEvent(FieldEvent.OnChangeTypeClick) }
)
Divider()
// Limit object types (only for OBJECT format)
if (field.format == RelationFormat.OBJECT) {
LimitTypesSection(
objTypes = field.limitObjectTypes,
isEditable = isEditable,
onLimitTypesClick = { fieldEvent(FieldEvent.OnLimitTypesClick) }
)
Divider()
}
Spacer(modifier = Modifier.height(14.dp))
if (isEditable) {
ButtonPrimary(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
text = stringResource(R.string.object_type_fields_btn_save),
onClick = {
fieldEvent(
FieldEvent.OnSaveButtonClicked(
name = innerValue,
format = field.format,
limitObjectTypes = field.limitObjectTypes.map { it.id }
)
)
},
size = ButtonSize.Large
)
}
}
}
@Composable
fun NameTextField(
modifier: Modifier,
value: String,
isEditable: Boolean,
focusRequester: FocusRequester,
keyboardController: SoftwareKeyboardController?,
onValueChange: (String) -> Unit
) {
val focusManager = LocalFocusManager.current
Column(modifier = modifier) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
text = stringResource(id = R.string.name),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary)
)
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = HeadlineHeading.copy(color = colorResource(id = R.color.text_primary)),
singleLine = true,
enabled = isEditable,
cursorBrush = SolidColor(colorResource(id = R.color.text_primary)),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 20.dp, top = 6.dp, end = 20.dp)
.focusRequester(focusRequester)
.onFocusChanged { /* You can handle focus changes here if needed */ },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions {
keyboardController?.hide()
focusManager.clearFocus()
onValueChange(value)
},
decorationBox = { innerTextField ->
if (value.isEmpty()) {
Text(
text = stringResource(id = R.string.untitled),
style = HeadlineHeading,
color = colorResource(id = R.color.text_tertiary),
modifier = Modifier.fillMaxWidth()
)
}
innerTextField()
}
)
}
}
@Composable
fun FieldTypeSection(
format: RelationFormat,
isEditable: Boolean,
onTypeClick: () -> Unit
) {
val icon = format.simpleIcon()
SectionItem(
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
text = stringResource(id = R.string.type)
)
Divider()
Box(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.padding(horizontal = 20.dp)
.noRippleThrottledClickable { if (isEditable) onTypeClick() }
) {
if (icon != null) {
Image(
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterStart),
painter = painterResource(id = icon),
contentDescription = "Relation format icon"
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 34.dp)
.align(Alignment.CenterStart),
text = stringResource(format.getPrettyName()),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (isEditable) {
Image(
modifier = Modifier.align(Alignment.CenterEnd),
painter = painterResource(id = R.drawable.ic_arrow_forward_24),
contentDescription = "Change field format icon"
)
}
}
}
@Composable
fun LimitTypesSection(
objTypes: List<UiFieldObjectItem>,
isEditable: Boolean,
onLimitTypesClick: () -> Unit
) {
val size = objTypes.size
SectionItem(
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
text = stringResource(id = R.string.limit_object_types)
)
Divider()
Box(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.padding(horizontal = 20.dp)
.noRippleThrottledClickable { if (isEditable) onLimitTypesClick() }
) {
if (objTypes.isNotEmpty()) {
Row(modifier = Modifier.align(Alignment.CenterStart)) {
ListWidgetObjectIcon(
modifier = Modifier.size(20.dp),
icon = objTypes.first().icon,
backgroundColor = R.color.transparent_black
)
Text(
modifier = Modifier.padding(start = 6.dp),
text = objTypes.first().title.take(20),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (size > 1) {
Text(
text = " +${size - 1}",
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
} else {
Text(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterStart),
text = stringResource(R.string.none),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (isEditable) {
Image(
modifier = Modifier.align(Alignment.CenterEnd),
painter = painterResource(id = R.drawable.ic_arrow_forward_24),
contentDescription = "Change field object types icon"
)
}
}
}
@Composable
fun SectionItem(modifier: Modifier, text: String) {
Box(modifier = modifier) {
Text(
modifier = Modifier
.padding(start = 20.dp, bottom = 8.dp)
.align(Alignment.BottomStart),
text = text,
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary)
)
}
}
@DefaultPreviews
@Composable
private fun MyPreview() {
EditFieldScreen(
modifier = Modifier.fillMaxWidth(),
uiFieldEditOrNewState = UiFieldEditOrNewState.Visible.Edit(
item = createDummyFieldDraggableItem()
),
fieldEvent = {}
)
}
@DefaultPreviews
@Composable
private fun MyPreviewOnlyPreview() {
EditFieldScreen(
modifier = Modifier.fillMaxWidth(),
uiFieldEditOrNewState = UiFieldEditOrNewState.Visible.ViewOnly(
item = createDummyFieldDraggableItem(
isEditableField = false
)
),
fieldEvent = {}
)
}

View file

@ -35,24 +35,19 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedback
import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedbackType
import com.anytypeio.anytype.core_ui.common.bottomBorder
import com.anytypeio.anytype.core_ui.common.rememberReorderHapticFeedback
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
import com.anytypeio.anytype.core_ui.foundation.Dragger
@ -68,14 +63,14 @@ import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent.*
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent.FieldItemMenu.*
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState
import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Section
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState
import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState
import com.anytypeio.anytype.feature_object_type.ui.UiIconState
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
import com.anytypeio.anytype.feature_properties.edit.UiEditPropertyState
import com.anytypeio.anytype.feature_properties.edit.ui.PropertyScreen
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import kotlinx.coroutines.delay
import sh.calvin.reorderable.ReorderableItem
@ -88,9 +83,8 @@ fun FieldsMainScreen(
uiFieldsListState: UiFieldsListState,
uiTitleState: UiTitleState,
uiIconState: UiIconState,
uiFieldEditOrNewState: UiFieldEditOrNewState,
uiFieldLocalInfoState: UiLocalsFieldsInfoState,
uiAddFieldsScreenState: UiAddFieldsScreenState,
uiEditPropertyState: UiEditPropertyState,
fieldEvent: (FieldEvent) -> Unit
) {
@ -198,10 +192,7 @@ fun FieldsMainScreen(
item = item,
reorderingState = reorderableLazyColumnState,
fieldEvent = fieldEvent,
isReorderable = false,
onAddIconClick = {
fieldEvent(FieldEvent.Section.OnAddToHeaderIconClick)
}
isReorderable = false
)
}
is Section.Local,
@ -226,11 +217,16 @@ fun FieldsMainScreen(
}
)
if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) {
EditFieldScreen(
if (uiEditPropertyState is UiEditPropertyState.Visible) {
PropertyScreen(
modifier = Modifier.fillMaxWidth(),
uiFieldEditOrNewState = uiFieldEditOrNewState,
fieldEvent = fieldEvent
uiState = uiEditPropertyState,
onDismissRequest = { fieldEvent(OnEditPropertyScreenDismiss) },
onFormatClick = {},
onLimitTypesClick = {},
onSaveButtonClicked = {},
onCreateNewButtonClicked = {},
onPropertyNameUpdate = { }
)
}
@ -241,13 +237,6 @@ fun FieldsMainScreen(
fieldEvent = fieldEvent
)
}
if (uiAddFieldsScreenState is UiAddFieldsScreenState.Visible) {
AddFieldScreen(
state = uiAddFieldsScreenState,
fieldEvent = fieldEvent
)
}
}
/** Returns a content type string based on the item type. **/
@ -599,29 +588,6 @@ private fun LazyItemScope.FieldItemDraggable(
}
}
@Composable
fun Modifier.bottomBorder(
strokeWidth: Dp = 0.5.dp,
color: Color = colorResource(R.color.shape_primary)
) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height - strokeWidthPx / 2
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
}
}
)
@Composable
fun ItemDropDownMenu(
item: UiFieldsListItem.Item,
@ -757,9 +723,8 @@ fun PreviewTypeFieldsMainScreen() {
)
),
fieldEvent = {},
uiFieldEditOrNewState = UiFieldEditOrNewState.Hidden,
uiFieldLocalInfoState = UiLocalsFieldsInfoState.Hidden,
uiAddFieldsScreenState = UiAddFieldsScreenState.Hidden
uiEditPropertyState = UiEditPropertyState.Hidden,
uiFieldLocalInfoState = UiLocalsFieldsInfoState.Hidden
)
}

View file

@ -1,39 +0,0 @@
package com.anytypeio.anytype.feature_object_type.ui
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
import com.anytypeio.anytype.presentation.objects.ObjectIcon
fun createDummyFieldDraggableItem(isEditableField: Boolean = true): UiFieldsListItem.Item.Draggable {
return UiFieldsListItem.Item.Draggable(
id = "dummyId",
fieldKey = "dummyKey",
fieldTitle = "Field Title",
format = RelationFormat.OBJECT,
limitObjectTypes = listOf(
UiFieldObjectItem(
id = "dummyObjectId1",
key = "dummyKey1",
title = "Dummy Object Type 1",
icon = ObjectIcon.Empty.ObjectType,
),
UiFieldObjectItem(
id = "dummyObjectId1",
key = "dummyKey1",
title = "Dummy Object Type 1",
icon = ObjectIcon.Empty.ObjectType,
),
UiFieldObjectItem(
id = "dummyObjectId1",
key = "dummyKey1",
title = "Dummy Object Type 1",
icon = ObjectIcon.Empty.ObjectType,
),
),
isEditableField = isEditableField,
canDelete = true
)
}

View file

@ -33,8 +33,7 @@ sealed class ObjectTypeCommand {
data object OpenFieldsScreen : ObjectTypeCommand()
data class OpenAddFieldScreen(val typeId: Id, val space: Id, val isSet: Boolean = false) :
ObjectTypeCommand()
data class OpenEditTypePropertiesScreen(val typeId: Id, val space: Id) : ObjectTypeCommand()
}
//region OBJECT TYPE HEADER (title + icon)

View file

@ -13,11 +13,10 @@ import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Item
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Section
import com.anytypeio.anytype.feature_properties.edit.UiPropertyLimitTypeItem
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.mapper.objectIcon
import com.anytypeio.anytype.presentation.relations.BasicObjectCoverWrapper
@ -56,7 +55,7 @@ fun ObjectWrapper.Basic.toTemplateView(
* Extension function to safely get a name for the relation.
* If the name is blank, returns a default untitled title.
*/
private fun ObjectWrapper.Relation.getName(stringResourceProvider: StringResourceProvider): String =
fun ObjectWrapper.Relation.getName(stringResourceProvider: StringResourceProvider): String =
if (name.isNullOrBlank()) {
stringResourceProvider.getUntitledObjectTitle()
} else {
@ -140,7 +139,7 @@ suspend fun buildUiFieldsList(
}
return buildList {
add(Section.Header(canAdd = true))
add(Section.Header(canAdd = false))
addAll(headerItems)
add(Section.SideBar(canAdd = true))
@ -173,11 +172,11 @@ private suspend fun mapLimitObjectTypes(
storeOfObjectTypes: StoreOfObjectTypes,
fieldParser: FieldParser,
urlBuilder: UrlBuilder
): List<UiFieldObjectItem> {
): List<UiPropertyLimitTypeItem> {
return if (relation.format == RelationFormat.OBJECT && relation.relationFormatObjectTypes.isNotEmpty()) {
relation.relationFormatObjectTypes.mapNotNull { key ->
storeOfObjectTypes.getByKey(key)?.let { objType ->
UiFieldObjectItem(
UiPropertyLimitTypeItem(
id = objType.id,
key = objType.uniqueKey,
title = fieldParser.getObjectName(objType),
@ -239,16 +238,3 @@ private suspend fun mapToUiFieldsLocalListItem(
)
}
fun ObjectWrapper.Relation.mapToUiAddFieldListItem(
stringResourceProvider: StringResourceProvider
): UiAddFieldItem? {
val field = this
if (field.key == Relations.DESCRIPTION) return null
return UiAddFieldItem(
id = field.id,
fieldKey = field.key,
fieldTitle = field.getName(stringResourceProvider),
format = field.format
)
}

View file

@ -4,14 +4,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.permissions.ObjectPermissions
import com.anytypeio.anytype.core_models.permissions.toObjectPermissionsForTypes
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
import com.anytypeio.anytype.domain.library.StoreSearchParams
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
@ -22,17 +21,12 @@ import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.DeleteObjects
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields
import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields
import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.templates.CreateTemplate
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState
import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState
import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState.Visible.*
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState
@ -50,19 +44,15 @@ import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState.*
import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesAddIconState
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesButtonState
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesModalListState
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
import com.anytypeio.anytype.feature_object_type.ui.buildUiFieldsList
import com.anytypeio.anytype.feature_object_type.ui.mapToUiAddFieldListItem
import com.anytypeio.anytype.feature_object_type.ui.toTemplateView
import com.anytypeio.anytype.feature_properties.edit.UiEditPropertyState
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.cover.UnsplashViewModel.Companion.DEBOUNCE_DURATION
import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenObjectType
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.presentation.home.navigation
import com.anytypeio.anytype.presentation.mapper.objectIcon
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
@ -71,22 +61,14 @@ import com.anytypeio.anytype.presentation.sync.updateStatus
import com.anytypeio.anytype.presentation.templates.TemplateView
import kotlin.collections.map
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import timber.log.Timber
@ -105,18 +87,15 @@ class ObjectTypeViewModel(
private val storeOfObjectTypes: StoreOfObjectTypes,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
private val createObject: CreateObject,
private val fieldParser: FieldParser,
private val coverImageHashProvider: CoverImageHashProvider,
private val deleteObjects: DeleteObjects,
private val setObjectDetails: SetObjectDetails,
private val createObjectSet: CreateObjectSet,
private val stringResourceProvider: StringResourceProvider,
private val createTemplate: CreateTemplate,
private val duplicateObjects: DuplicateObjects,
private val getObjectTypeConflictingFields: GetObjectTypeConflictingFields,
private val objectTypeSetRecommendedFields: SetObjectTypeRecommendedFields,
private val objectTypeSetHeaderRecommendedFields: SetObjectTypeHeaderRecommendedFields
private val objectTypeSetRecommendedFields: SetObjectTypeRecommendedFields
) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
//region UI STATE
@ -148,13 +127,11 @@ class ObjectTypeViewModel(
val uiFieldLocalInfoState =
MutableStateFlow<UiLocalsFieldsInfoState>(UiLocalsFieldsInfoState.Hidden)
//fields
//properties list
val uiFieldsListState = MutableStateFlow<UiFieldsListState>(UiFieldsListState.EMPTY)
val uiFieldEditOrNewState =
MutableStateFlow<UiFieldEditOrNewState>(UiFieldEditOrNewState.Hidden)
//add new field
val uiAddFieldsState = MutableStateFlow<UiAddFieldsScreenState>(UiAddFieldsScreenState.Hidden)
//edit property
val uiEditPropertyScreen = MutableStateFlow<UiEditPropertyState>(UiEditPropertyState.Hidden)
//error
val errorState = MutableStateFlow<UiErrorState>(UiErrorState.Hidden)
@ -166,6 +143,8 @@ class ObjectTypeViewModel(
private val _objectTypeConflictingFieldIds = MutableStateFlow<List<Id>>(emptyList())
//endregion
val commands = MutableSharedFlow<ObjectTypeCommand>()
//region INIT AND LIFE CYCLE
init {
Timber.d("init, vmParams: $vmParams")
@ -191,7 +170,6 @@ class ObjectTypeViewModel(
//endregion
//region DATA
private fun proceedWithObservingObjectType() {
viewModelScope.launch {
combine(
@ -387,6 +365,31 @@ class ObjectTypeViewModel(
fun hideError() {
errorState.value = UiErrorState.Hidden
}
fun setupUiEditPropertyScreen(item: UiFieldsListItem.Item) {
val permissions = _objectTypePermissionsState.value
if (permissions?.participantCanEdit == true && item.isEditableField) {
uiEditPropertyScreen.value = UiEditPropertyState.Visible.Edit(
id = item.id,
key = item.fieldKey,
name = item.fieldTitle,
formatName = stringResourceProvider.getPropertiesFormatPrettyString(item.format),
formatIcon = item.format.simpleIcon(),
format = item.format,
limitObjectTypes = item.limitObjectTypes
)
} else {
uiEditPropertyScreen.value = UiEditPropertyState.Visible.View(
id = item.id,
key = item.fieldKey,
name = item.fieldTitle,
formatName = stringResourceProvider.getPropertiesFormatPrettyString(item.format),
formatIcon = item.format.simpleIcon(),
format = item.format,
limitObjectTypes = item.limitObjectTypes
)
}
}
//endregion
//region Ui EVENTS - TYPES
@ -623,33 +626,17 @@ class ObjectTypeViewModel(
fun onFieldEvent(event: FieldEvent) {
Timber.d("onFieldEvent: $event")
when (event) {
FieldEvent.OnChangeTypeClick -> TODO()
FieldEvent.OnFieldEditScreenDismiss -> {
uiFieldEditOrNewState.value = UiFieldEditOrNewState.Hidden
FieldEvent.OnEditPropertyScreenDismiss -> {
uiEditPropertyScreen.value = UiEditPropertyState.Hidden
}
is FieldEvent.OnFieldItemClick -> {
when (event.item) {
is UiFieldsListItem.Item -> {
val permissions = _objectTypePermissionsState.value
if (permissions?.participantCanEdit == true && event.item.isEditableField) {
uiFieldEditOrNewState.value = Edit(
event.item
)
} else {
uiFieldEditOrNewState.value = ViewOnly(
event.item
)
}
}
is UiFieldsListItem.Item -> setupUiEditPropertyScreen(item = event.item)
else -> {}
}
}
FieldEvent.OnLimitTypesClick -> TODO()
is FieldEvent.OnSaveButtonClicked -> TODO()
is FieldEvent.FieldItemMenu -> proceedWithFieldItemMenuClick(event)
FieldEvent.FieldLocalInfo.OnDismiss -> {
uiFieldLocalInfoState.value = UiLocalsFieldsInfoState.Hidden
@ -659,12 +646,15 @@ class ObjectTypeViewModel(
uiFieldLocalInfoState.value = UiLocalsFieldsInfoState.Visible
}
FieldEvent.Section.OnAddToHeaderIconClick -> {
proceedWithAddFieldToHeaderScreen()
}
FieldEvent.Section.OnAddToSidebarIconClick -> {
proceedWithAddFieldToSidebarScreen()
viewModelScope.launch {
commands.emit(
ObjectTypeCommand.OpenEditTypePropertiesScreen(
typeId = vmParams.objectId,
space = vmParams.spaceId.id,
)
)
}
}
FieldEvent.DragEvent.OnDragEnd -> {
@ -706,24 +696,6 @@ class ObjectTypeViewModel(
currentList.add(toIndex, item)
uiFieldsListState.value = UiFieldsListState(items = currentList)
}
FieldEvent.OnAddFieldScreenDismiss -> {
hideAddNewFieldScreen()
}
is FieldEvent.OnAddToHeaderFieldClick -> {
onAddToHeaderFieldClicked(item = event.item)
hideAddNewFieldScreen()
}
is FieldEvent.OnAddToSidebarFieldClick -> {
onAddToSidebarFieldClicked(item = event.item)
hideAddNewFieldScreen()
}
is FieldEvent.OnAddFieldSearchQueryChanged -> {
onQueryChanged(query = event.query)
}
}
}
@ -777,30 +749,6 @@ class ObjectTypeViewModel(
proceedWithSetRecommendedFields(newRecommendedFields)
}
is FieldEvent.FieldItemMenu.OnRemoveLocalClick -> TODO()
}
}
//endregion
//region NAVIGATION
val commands = MutableSharedFlow<ObjectTypeCommand>()
val navigation = MutableSharedFlow<OpenObjectNavigation>()
private fun proceedWithNavigation(
objectId: Id,
objectLayout: ObjectType.Layout?
) {
Timber.d("proceedWithNavigation, objectId: $objectId, objectLayout: $objectLayout")
val destination = objectLayout?.navigation(
target = objectId,
space = vmParams.spaceId.id
)
if (destination != null) {
viewModelScope.launch {
navigation.emit(destination)
}
} else {
Timber.w("No navigation destination found for object $objectId with layout $objectLayout")
}
}
//endregion
@ -960,124 +908,6 @@ class ObjectTypeViewModel(
)
}
}
private fun proceedWithSetHeaderRecommendedFields(fields: List<Id>) {
val params = SetObjectTypeHeaderRecommendedFields.Params(
objectTypeId = vmParams.objectId,
fields = fields
)
viewModelScope.launch {
objectTypeSetHeaderRecommendedFields.async(params).fold(
onSuccess = {
Timber.d("Header recommended fields set")
},
onFailure = {
Timber.e(it, "Error while setting header recommended fields")
}
)
}
}
//endregion
//region ADD NEW FIELD
private val input = MutableStateFlow("")
@OptIn(FlowPreview::class)
private val query = input.take(1).onCompletion {
emitAll(
input.drop(1).debounce(DEBOUNCE_DURATION).distinctUntilChanged()
)
}
private var addFieldSearchJob: Job? = null
/**
* Loads the available fields from type, applies filtering based on a search query,
* and then updates the UI state.
*/
private fun showAddFieldScreen(addToHeader: Boolean) {
// Collect field keys that are already present in the type fields list.
val typeFieldsKeys =
uiFieldsListState.value.items.mapNotNull { (it as? UiFieldsListItem.Item)?.fieldKey }
addFieldSearchJob = viewModelScope.launch {
// Combine the search query flow with the list of all fields.
combine(
query,
storeOfRelations.trackChanges()
) { queryText, _ ->
// Filter out fields by query and that already exist and are not valid.
filterFields(
allFields = storeOfRelations.getAll(),
typeKeys = typeFieldsKeys,
queryText = queryText
)
}.collect { filteredFields ->
val items = filteredFields.mapNotNull { field ->
field.mapToUiAddFieldListItem(stringResourceProvider)
}.sortedBy { it.fieldTitle }
uiAddFieldsState.value = UiAddFieldsScreenState.Visible(
items = items,
addToHeader = addToHeader
)
}
}
}
private fun filterFields(
allFields: List<ObjectWrapper.Relation>,
typeKeys: List<Key>,
queryText: String
): List<ObjectWrapper.Relation> = allFields.filter { field ->
field.key !in typeKeys &&
field.isValidToUse &&
(queryText.isBlank() || field.name?.contains(queryText, ignoreCase = true) == true)
}
private fun onQueryChanged(query: String) {
input.value = query
}
fun hideAddNewFieldScreen() {
input.value = ""
addFieldSearchJob?.cancel()
addFieldSearchJob = null
uiAddFieldsState.value = UiAddFieldsScreenState.Hidden
}
fun proceedWithAddFieldToHeaderScreen() {
showAddFieldScreen(addToHeader = true)
}
fun proceedWithAddFieldToSidebarScreen() {
showAddFieldScreen(addToHeader = false)
}
private fun updateFieldRecommendations(
currentFields: List<String>?,
item: UiAddFieldItem,
updateAction: (List<String>) -> Unit
) {
val newFields = currentFields.orEmpty() + item.id
updateAction(newFields)
}
private fun onAddToSidebarFieldClicked(item: UiAddFieldItem) {
updateFieldRecommendations(
currentFields = _objTypeState.value?.recommendedRelations,
item = item,
updateAction = ::proceedWithSetRecommendedFields
)
}
private fun onAddToHeaderFieldClicked(item: UiAddFieldItem) {
updateFieldRecommendations(
currentFields = _objTypeState.value?.recommendedFeaturedRelations,
item = item,
updateAction = ::proceedWithSetHeaderRecommendedFields
)
}
//endregion
companion object {

View file

@ -3,7 +3,6 @@ package com.anytypeio.anytype.feature_object_type.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.UrlBuilder
@ -13,10 +12,8 @@ import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.DeleteObjects
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields
import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields
import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.templates.CreateTemplate
@ -35,18 +32,15 @@ class ObjectTypeVMFactory @Inject constructor(
private val storeOfObjectTypes: StoreOfObjectTypes,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
private val createObject: CreateObject,
private val fieldParser: FieldParser,
private val coverImageHashProvider: CoverImageHashProvider,
private val deleteObjects: DeleteObjects,
private val setObjectDetails: SetObjectDetails,
private val createObjectSet: CreateObjectSet,
private val stringResourceProvider: StringResourceProvider,
private val createTemplate: CreateTemplate,
private val duplicateObjects: DuplicateObjects,
private val getObjectTypeConflictingFields: GetObjectTypeConflictingFields,
private val objectTypeSetRecommendedFields: SetObjectTypeRecommendedFields,
private val objectTypeSetHeaderRecommendedFields: SetObjectTypeHeaderRecommendedFields
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -61,17 +55,14 @@ class ObjectTypeVMFactory @Inject constructor(
storeOfObjectTypes = storeOfObjectTypes,
storelessSubscriptionContainer = storelessSubscriptionContainer,
spaceSyncAndP2PStatusProvider = spaceSyncAndP2PStatusProvider,
createObject = createObject,
fieldParser = fieldParser,
coverImageHashProvider = coverImageHashProvider,
deleteObjects = deleteObjects,
setObjectDetails = setObjectDetails,
createObjectSet = createObjectSet,
stringResourceProvider = stringResourceProvider,
createTemplate = createTemplate,
duplicateObjects = duplicateObjects,
getObjectTypeConflictingFields = getObjectTypeConflictingFields,
objectTypeSetRecommendedFields = objectTypeSetRecommendedFields,
objectTypeSetHeaderRecommendedFields = objectTypeSetHeaderRecommendedFields
) as T
}

View file

@ -193,29 +193,6 @@ class TestFieldsMappping {
)
}
@Test
fun `should not filter sidebar fields by hidden`() = runTest {
storeOfRelations.apply {
merge(allSpaceRelations)
}
storeOfObjectTypes.apply {
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
}
val parsedFields = fieldParser.getObjectTypeParsedFields(
objectType = testObjectType,
storeOfRelations = storeOfRelations,
objectTypeConflictingFieldsIds = listOf()
)
assertEquals(
expected = listOf(field3, field4, field5),
actual = parsedFields.sidebar
)
}
@Test
fun `should map hidden fields`() = runTest {
@ -233,8 +210,54 @@ class TestFieldsMappping {
objectTypeConflictingFieldsIds = listOf()
)
assertEquals(
expected = listOf(),
actual = parsedFields.hidden
)
}
@Test
fun `test filter duplicates`() = runTest {
storeOfRelations.apply {
merge(allSpaceRelations)
}
val testObjectType = StubObjectType(
recommendedFeaturedRelations = listOf(field1, field1, field2, field3).map { it.id },
recommendedRelations = listOf(field1, field4, field4).map { it.id },
recommendedFileRelations = listOf(field1, field2, field3, field4, field5, field5).map { it.id },
recommendedHiddenRelations = listOf(field1, field2, field3, field4, field5, fieldCreatedDate, fieldCreatedDate).map { it.id },
space = space
)
storeOfObjectTypes.apply {
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
}
val parsedFields = fieldParser.getObjectTypeParsedFields(
objectType = testObjectType,
storeOfRelations = storeOfRelations,
objectTypeConflictingFieldsIds = listOf()
)
assertEquals(
expected = listOf(field1, field2, field3),
actual = parsedFields.header
)
assertEquals(
expected = listOf(field4),
actual = parsedFields.sidebar
)
assertEquals(
expected = listOf(field5),
actual = parsedFields.file
)
assertEquals(
expected = listOf(fieldCreatedDate),
actual = parsedFields.hidden
)
}

View file

@ -0,0 +1,58 @@
plugins {
id "com.android.library"
id "kotlin-android"
alias(libs.plugins.compose.compiler)
}
android {
defaultConfig {
buildConfigField "boolean", "USE_NEW_WINDOW_INSET_API", "true"
buildConfigField "boolean", "USE_EDGE_TO_EDGE", "true"
}
buildFeatures {
compose true
}
namespace 'com.anytypeio.anytype.feature_properties'
}
dependencies {
implementation project(':domain')
implementation project(':core-ui')
implementation project(':analytics')
implementation project(':core-models')
implementation project(':core-utils')
implementation project(':localization')
implementation project(':presentation')
compileOnly libs.javaxInject
implementation libs.lifecycleViewModel
implementation libs.lifecycleRuntime
implementation libs.appcompat
implementation libs.compose
implementation libs.fragmentCompose
implementation libs.composeFoundation
implementation libs.composeToolingPreview
implementation libs.composeMaterial3
implementation libs.navigationCompose
debugImplementation libs.composeTooling
implementation libs.timber
testImplementation project(':test:android-utils')
testImplementation project(':test:utils')
testImplementation project(":test:core-models-stub")
testImplementation libs.junit
testImplementation libs.kotlinTest
testImplementation libs.androidXTestCore
testImplementation libs.mockitoKotlin
testImplementation libs.coroutineTesting
testImplementation libs.timberJUnit
testImplementation libs.turbine
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest/>

View file

@ -0,0 +1,5 @@
package com.anytypeio.anytype.feature_properties
class EditSpacePropertiesViewModel {
//todo: implement later
}

View file

@ -0,0 +1,22 @@
package com.anytypeio.anytype.feature_properties
import androidx.lifecycle.ViewModel
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields
import com.anytypeio.anytype.domain.relations.CreateRelation
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams
class EditTypePropertiesViewModel(
private val vmParams: EditTypePropertiesVmParams,
private val storeOfRelations: StoreOfRelations,
private val storeOfObjectTypes: StoreOfObjectTypes,
private val stringResourceProvider: StringResourceProvider,
private val createRelation: CreateRelation,
private val setObjectDetails: SetObjectDetails,
private val setObjectTypeRecommendedFields: SetObjectTypeRecommendedFields
) : ViewModel() {
}

View file

@ -0,0 +1,35 @@
package com.anytypeio.anytype.feature_properties
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields
import com.anytypeio.anytype.domain.relations.CreateRelation
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams
import javax.inject.Inject
class EditTypePropertiesViewModelFactory @Inject constructor(
private val vmParams: EditTypePropertiesVmParams,
private val storeOfRelations: StoreOfRelations,
private val stringResourceProvider: StringResourceProvider,
private val createRelation: CreateRelation,
private val setObjectDetails: SetObjectDetails,
private val storeOfObjectTypes: StoreOfObjectTypes,
private val setObjectTypeRecommendedFields: SetObjectTypeRecommendedFields
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
EditTypePropertiesViewModel(
vmParams = vmParams,
storeOfRelations = storeOfRelations,
stringResourceProvider = stringResourceProvider,
createRelation = createRelation,
setObjectDetails = setObjectDetails,
storeOfObjectTypes = storeOfObjectTypes,
setObjectTypeRecommendedFields = setObjectTypeRecommendedFields
) as T
}

View file

@ -0,0 +1,12 @@
package com.anytypeio.anytype.feature_properties.add
sealed class UiEditTypePropertiesEvent {
data class OnSearchQueryChanged(val query: String) : UiEditTypePropertiesEvent()
data class OnCreate(val item: UiEditTypePropertiesItem.Create) : UiEditTypePropertiesEvent()
data class OnTypeClicked(val item: UiEditTypePropertiesItem.Format) : UiEditTypePropertiesEvent()
data class OnExistingClicked(val item: UiEditTypePropertiesItem.Default) : UiEditTypePropertiesEvent()
data object OnCreateNewButtonClicked : UiEditTypePropertiesEvent()
data object OnSaveButtonClicked : UiEditTypePropertiesEvent()
data object OnEditPropertyScreenDismissed : UiEditTypePropertiesEvent()
data class OnPropertyNameUpdate(val name: String) : UiEditTypePropertiesEvent()
}

View file

@ -0,0 +1,116 @@
package com.anytypeio.anytype.feature_properties.add
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.resources.StringResourceProvider
data class UiEditTypePropertiesState(
val items: List<UiEditTypePropertiesItem>
) {
companion object {
val EMPTY = UiEditTypePropertiesState(emptyList())
val DEFAULT_NEW_PROPERTY_FORMAT = RelationFormat.STATUS
//This is a list of formats that are available for creating new properties
val PROPERTIES_FORMATS = listOf<RelationFormat>(
RelationFormat.LONG_TEXT,
RelationFormat.TAG,
RelationFormat.STATUS,
RelationFormat.NUMBER,
RelationFormat.DATE,
RelationFormat.FILE,
RelationFormat.OBJECT,
RelationFormat.CHECKBOX,
RelationFormat.URL,
RelationFormat.EMAIL,
RelationFormat.PHONE
)
}
}
sealed class UiEditTypePropertiesItem {
abstract val id: Id
sealed class Section : UiEditTypePropertiesItem() {
data class Types(
override val id: Id = "section_properties_types_id"
) : Section()
data class Existing(
override val id: Id = "section_properties_existing_id"
) : Section()
}
data class Format(
override val id: Id = "property_item_format_id_${format.ordinal}",
val format: RelationFormat,
val prettyName: String
) : UiEditTypePropertiesItem()
data class Create(
override val id: Id = ID,
val format: RelationFormat = UiEditTypePropertiesState.Companion.DEFAULT_NEW_PROPERTY_FORMAT,
val title: String
) : UiEditTypePropertiesItem() {
companion object {
private const val ID = "create_new_property_id"
}
}
data class Default(
override val id: Id,
val format: RelationFormat,
val propertyKey: Key,
val title: String,
) : UiEditTypePropertiesItem()
}
data class EditTypePropertiesVmParams(
val objectTypeId: Id,
val spaceId: SpaceId
)
sealed class UiEditTypePropertiesErrorState {
data object Hidden : UiEditTypePropertiesErrorState()
data class Show(val reason: Reason) : UiEditTypePropertiesErrorState()
sealed class Reason {
data class ErrorAddingProperty(val msg: String) : Reason()
data class ErrorCreatingProperty(val msg: String) : Reason()
data class ErrorUpdatingProperty(val msg: String) : Reason()
data class Other(val msg: String) : Reason()
}
}
//region MAPPING
fun ObjectWrapper.Relation.mapToStateItem(
stringResourceProvider: StringResourceProvider
): UiEditTypePropertiesItem.Default? {
val field = this
if (field.key == Relations.DESCRIPTION) return null
return UiEditTypePropertiesItem.Default(
id = field.id,
propertyKey = field.key,
title = field.getName(stringResourceProvider),
format = field.format
)
}
/**
* Extension function to safely get a name for the relation.
* If the name is blank, returns a default untitled title.
*/
fun ObjectWrapper.Relation.getName(stringResourceProvider: StringResourceProvider): String =
if (name.isNullOrBlank()) {
stringResourceProvider.getUntitledObjectTitle()
} else {
name!!
}
//endregion

View file

@ -0,0 +1,16 @@
package com.anytypeio.anytype.feature_properties.add.ui
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import com.anytypeio.anytype.feature_properties.add.UiEditTypePropertiesEvent
import com.anytypeio.anytype.feature_properties.add.UiEditTypePropertiesState
import com.anytypeio.anytype.feature_properties.edit.UiEditPropertyState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddFieldScreen(
state: UiEditTypePropertiesState,
uiStateEditProperty: UiEditPropertyState,
event: (UiEditTypePropertiesEvent) -> Unit
) {
}

View file

@ -0,0 +1,44 @@
package com.anytypeio.anytype.feature_properties.edit
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.presentation.objects.ObjectIcon
sealed class UiEditPropertyState {
data object Hidden : UiEditPropertyState()
sealed class Visible : UiEditPropertyState() {
data class Edit(
val id: Id,
val key: Key,
val name: String,
val formatName: String,
val formatIcon: Int?,
val format: RelationFormat,
val limitObjectTypes: List<UiPropertyLimitTypeItem> = emptyList()
) : Visible()
data class New(
val name: String,
val formatName: String,
val formatIcon: Int?,
val format: RelationFormat,
val limitObjectTypes: List<UiPropertyLimitTypeItem> = emptyList()
) : Visible()
data class View(
val id: Id,
val key: Key,
val name: String,
val formatName: String,
val formatIcon: Int?,
val format: RelationFormat,
val limitObjectTypes: List<UiPropertyLimitTypeItem> = emptyList()
) : Visible()
}
}
data class UiPropertyLimitTypeItem(
val id: Id, val key: Key, val title: String, val icon: ObjectIcon
)

View file

@ -0,0 +1,20 @@
package com.anytypeio.anytype.feature_properties.edit.ui
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.anytypeio.anytype.feature_properties.edit.UiEditPropertyState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PropertyScreen(
modifier: Modifier,
uiState: UiEditPropertyState.Visible,
onSaveButtonClicked: () -> Unit = {},
onFormatClick: () -> Unit = {},
onLimitTypesClick: () -> Unit = {},
onCreateNewButtonClicked: () -> Unit = {},
onDismissRequest: () -> Unit,
onPropertyNameUpdate: (String) -> Unit
) {
}

View file

@ -332,11 +332,11 @@
<string name="relation_format_status">Select</string>
<string name="relation_format_file">File &amp; Media</string>
<string name="relation_format_checkbox">Checkbox</string>
<string name="relation_format_url">Url</string>
<string name="relation_format_url">URL</string>
<string name="relation_format_email">Email</string>
<string name="relation_format_phone">Phone</string>
<string name="relation_format_phone">Phone number</string>
<string name="relation_format_emoji">Emoji</string>
<string name="relation_format_object">Object</string>
<string name="relation_format_object">Object relation</string>
<string name="relation_format_relation">Relation</string>
<string name="viewer_sorting_prefix">Then</string>
<string name="enter_value">Enter value</string>
@ -344,7 +344,7 @@
<string name="add_sort">Add a sort</string>
<string name="apply">Apply</string>
<string name="add_filter">Add a filter</string>
<string name="relation_format_tag">Multiselect</string>
<string name="relation_format_tag">Multi-Select</string>
<string name="unknown_number_of_objects">Unknown number of objects</string>
<string name="sort_ascending">Ascending</string>
<string name="sort_descending">Descending</string>
@ -366,7 +366,7 @@
<string name="unknown_object_type">Unknown type</string>
<string name="relation_format_short_text">Short text</string>
<string name="relation_format_long_text">Long text</string>
<string name="relation_format_long_text">Text</string>
<string name="set_new_relation">Set new relation</string>
<string name="relation_deleted">Deleted relation</string>
@ -918,6 +918,7 @@
<string name="generic_error">Something went wrong. Please try again.</string>
<string name="type">Type</string>
<string name="format">Format</string>
<string name="space_info">Space info</string>
<string name="creation_date">Creation date</string>
<string name="created_by">Created by</string>
@ -1964,4 +1965,18 @@ Please provide specific details of your needs here.</string>
<string name="space_settings_icon_title">Edit picture</string>
<string name="space_settings_save_button">Save</string>
<string name="object_type_add_property_screen_title">Add property</string>
<string name="object_type_add_property_screen_section">Properties types</string>
<string name="object_type_add_property_screen_create">Create property \'%1$s\'"</string>
<string name="object_type_add_property_screen_section_types">Properties types"</string>
<string name="object_type_add_property_screen_section_existing">Existing properties"</string>
<string name="object_type_add_property_screen_search_hint">Search or create new"</string>
<string name="new_property_hint">New property</string>
<string name="edit_property_limit_objects">Limit objects to</string>
<string name="add_property_error_create_new">Error while creating new property</string>
<string name="add_property_error_update">Error while updating property</string>
<string name="add_property_error_add">Error while adding property to type</string>
</resources>

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.presentation.util
import android.content.Context
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.RelativeDate
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.presentation.R
@ -31,4 +32,23 @@ class StringResourceProviderImpl @Inject constructor(private val context: Contex
override fun getSetOfObjectsTitle(): String {
return context.getString(R.string.object_set_of_title)
}
override fun getPropertiesFormatPrettyString(format: RelationFormat): String {
return when (format) {
RelationFormat.LONG_TEXT, RelationFormat.SHORT_TEXT -> context.getString(R.string.relation_format_long_text)
RelationFormat.NUMBER -> context.getString(R.string.relation_format_number)
RelationFormat.STATUS -> context.getString(R.string.relation_format_status)
RelationFormat.TAG -> context.getString(R.string.relation_format_tag)
RelationFormat.DATE -> context.getString(R.string.relation_format_date)
RelationFormat.FILE -> context.getString(R.string.relation_format_file)
RelationFormat.CHECKBOX -> context.getString(R.string.relation_format_checkbox)
RelationFormat.URL -> context.getString(R.string.relation_format_url)
RelationFormat.EMAIL -> context.getString(R.string.relation_format_email)
RelationFormat.PHONE -> context.getString(R.string.relation_format_phone)
RelationFormat.EMOJI -> context.getString(R.string.relation_format_emoji)
RelationFormat.OBJECT -> context.getString(R.string.relation_format_object)
RelationFormat.RELATIONS -> context.getString(R.string.relation_format_relation)
RelationFormat.UNDEFINED -> context.getString(R.string.undefined)
}
}
}

View file

@ -67,3 +67,4 @@ include ':feature-chats'
include ':feature-all-content'
include ':feature-date'
include ':feature-object-type'
include ':feature-properties'

View file

@ -96,7 +96,7 @@ fun StubObjectType(
uniqueKey: String? = MockDataFactory.randomUuid(),
name: String = MockDataFactory.randomString(),
objectType: String = MockDataFactory.randomString(),
layout: Double = ObjectType.Layout.BASIC.code.toDouble(),
layout: Double = ObjectType.Layout.OBJECT_TYPE.code.toDouble(),
smartBlockTypes: List<Double> = emptyList(),
isDeleted: Boolean? = null,
isArchived: Boolean? = null,
@ -109,6 +109,7 @@ fun StubObjectType(
recommendedRelations: List<String> = emptyList(),
recommendedHiddenRelations: List<String> = emptyList(),
recommendedFeaturedRelations: List<String> = emptyList(),
recommendedFileRelations: List<String> = emptyList(),
space: Id? = null
): ObjectWrapper.Type = ObjectWrapper.Type(
map = mapOf(
@ -129,6 +130,7 @@ fun StubObjectType(
Relations.RECOMMENDED_RELATIONS to recommendedRelations,
Relations.RECOMMENDED_HIDDEN_RELATIONS to recommendedHiddenRelations,
Relations.RECOMMENDED_FEATURED_RELATIONS to recommendedFeaturedRelations,
Relations.RECOMMENDED_FILE_RELATIONS to recommendedFileRelations,
Relations.SPACE_ID to space
)
)