diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt index 94d37afc1b..e05b3daed4 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt @@ -805,6 +805,7 @@ class EditorViewModel( anchor = context, indent = INITIAL_INDENT, details = objectViewDetails, + participantCanEdit = permission?.isOwnerOrEditor() == true, restrictions = orchestrator.stores.objectRestrictions.current(), selection = currentSelection() ) { onRenderFlagFound -> flags.add(onRenderFlagFound) } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt index 7b80410911..2692da8209 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt @@ -1287,8 +1287,9 @@ sealed class BlockView : ViewType { data class FeaturedRelation( override val id: String, val relations: List, - val allowChangingObjectType: Boolean = true, - val isTodoLayout: Boolean = false + val allowChangingObjectType: Boolean = false, + val isTodoLayout: Boolean = false, + val hasFeaturePropertiesConflict: Boolean = false, ) : BlockView() { override fun getViewType(): Int = HOLDER_FEATURED_RELATION } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/BlockViewRenderer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/BlockViewRenderer.kt index 345c519614..bdd2897b4b 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/BlockViewRenderer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/BlockViewRenderer.kt @@ -33,6 +33,7 @@ interface BlockViewRenderer { selection: Set, count: Int = 0, parentScheme: NestedDecorationData = emptyList(), + participantCanEdit: Boolean = false, onRenderFlag: (RenderFlag) -> Unit = {}, ): List diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt index f5c3f29b07..62e9094ad1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt @@ -5,7 +5,6 @@ import com.anytypeio.anytype.core_models.Block.Content 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.ObjectTypeIds.BOOKMARK import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.ThemeColor @@ -30,7 +29,6 @@ import com.anytypeio.anytype.presentation.editor.editor.model.BlockView import com.anytypeio.anytype.presentation.editor.editor.model.BlockView.Appearance.InEditor import com.anytypeio.anytype.presentation.editor.editor.model.BlockView.Mode import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder -import com.anytypeio.anytype.presentation.extension.getTypeForObject import com.anytypeio.anytype.presentation.mapper.objectIcon import com.anytypeio.anytype.presentation.mapper.marks import com.anytypeio.anytype.presentation.mapper.toFileView @@ -39,13 +37,9 @@ import com.anytypeio.anytype.presentation.mapper.toVideoView import com.anytypeio.anytype.presentation.mapper.toView import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.objects.appearance.LinkAppearanceFactory -import com.anytypeio.anytype.presentation.objects.getFeaturedPropertiesIds -import com.anytypeio.anytype.presentation.objects.getProperType +import com.anytypeio.anytype.presentation.objects.toFeaturedPropertiesViews import com.anytypeio.anytype.presentation.relations.BasicObjectCoverWrapper -import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.relations.getCover -import com.anytypeio.anytype.presentation.relations.linksFeaturedRelation -import com.anytypeio.anytype.presentation.relations.objectTypeRelation import com.anytypeio.anytype.presentation.relations.view import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider import javax.inject.Inject @@ -74,6 +68,7 @@ class DefaultBlockViewRenderer @Inject constructor( selection: Set, count: Int, parentScheme: NestedDecorationData, + participantCanEdit: Boolean, onRenderFlag: (BlockViewRenderer.RenderFlag) -> Unit, ): List { @@ -710,10 +705,15 @@ class DefaultBlockViewRenderer @Inject constructor( is Content.FeaturedRelations -> { isPreviousBlockMedia = false mCounter = 0 - val featured = featured( - ctx = root.id, - block = block, - details = details + val featured = toFeaturedPropertiesViews( + objectId = root.id, + details = details, + storeOfRelations = storeOfRelations, + storeOfObjectTypes = storeOfObjectTypes, + urlBuilder = urlBuilder, + fieldParser = fieldParser, + blocks = listOf(block), + participantCanEdit = participantCanEdit ) if (!featured?.relations.isNullOrEmpty()) { @@ -2074,25 +2074,6 @@ class DefaultBlockViewRenderer @Inject constructor( } } - private suspend fun featured( - ctx: Id, - block: Block, - details: ObjectViewDetails - ): BlockView.FeaturedRelation? { - val obj = details.getObject(ctx) ?: return null - val views = mapFeaturedRelations( - ctx = ctx, - details = details, - currentObject = obj - ) - return BlockView.FeaturedRelation( - id = block.id, - relations = views, - allowChangingObjectType = obj.type.contains(BOOKMARK) != true, - isTodoLayout = obj.layout == ObjectType.Layout.TODO - ) - } - private fun workaroundGlobalNameOrIdentityRelation( featured: List, values: Map @@ -2116,68 +2097,6 @@ class DefaultBlockViewRenderer @Inject constructor( return result } - private suspend fun mapFeaturedRelations( - ctx: Id, - currentObject: ObjectWrapper.Basic, - details: ObjectViewDetails, - ): List { - - val objectFeaturedPropertiesKeys = currentObject.featuredRelations - - val featuredProperties = if (objectFeaturedPropertiesKeys.isNotEmpty()) { - objectFeaturedPropertiesKeys.mapNotNull { key -> - storeOfRelations.getByKey(key) - } - .sortedByDescending { it.key == Relations.TYPE } - } else { - currentObject.getFeaturedPropertiesIds( - storeOfRelations = storeOfRelations, - storeOfObjectTypes = storeOfObjectTypes, - fieldParser = fieldParser - ).mapNotNull { id -> - storeOfRelations.getById(id = id) - } - } - - return featuredProperties.mapNotNull { property -> - - when (property.key) { - Relations.DESCRIPTION -> null - Relations.TYPE -> { - val objectTypeId = details.getObject(ctx)?.getProperType() - if (objectTypeId != null) { - details.objectTypeRelation( - relationKey = property.key, - isFeatured = true, - objectTypeId = objectTypeId - ) - } else { - null - } - } - Relations.BACKLINKS, Relations.LINKS -> { - details.linksFeaturedRelation( - relations = storeOfRelations.getAll(), - ctx = ctx, - relationKey = property.key, - isFeatured = true - ) - } - else -> { - val values = details.getObject(ctx)?.map.orEmpty() - property.view( - details = details, - values = values, - urlBuilder = urlBuilder, - isFeatured = true, - fieldParser = fieldParser, - storeOfObjectTypes = storeOfObjectTypes - ) - } - } - } - } - private fun checkIfSelected( mode: Editor.Mode, block: Block, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/FeaturedPropertiesExt.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/FeaturedPropertiesExt.kt new file mode 100644 index 0000000000..abe2f42000 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/FeaturedPropertiesExt.kt @@ -0,0 +1,268 @@ +package com.anytypeio.anytype.presentation.objects + +import com.anytypeio.anytype.core_models.Block +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.ObjectViewDetails +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.permissions.toObjectPermissionsForTypes +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.domain.primitives.FieldParser +import com.anytypeio.anytype.presentation.editor.editor.model.BlockView +import com.anytypeio.anytype.presentation.extension.getObject +import com.anytypeio.anytype.presentation.extension.getTypeForObject +import com.anytypeio.anytype.presentation.relations.ObjectRelationView +import com.anytypeio.anytype.presentation.relations.isSystemKey +import com.anytypeio.anytype.presentation.relations.linksFeaturedRelation +import com.anytypeio.anytype.presentation.relations.view +import kotlin.collections.mapNotNull + +enum class ConflictResolutionStrategy { + MERGE, + OBJECT_ONLY +} + +/** + * Converts an object's featured properties into a [BlockView.FeaturedRelation] view. + * + * Retrieves the current object and its type from [details] using [objectId]. If the object's type is TEMPLATE, + * its target type is used as the effective type. The method then obtains the featured properties from the object + * (via keys) and parses the recommended featured property IDs from the effective type using [fieldParser]. It fetches + * the corresponding properties from [storeOfRelations] and checks for conflicts (i.e. any property key not equal + * to [Relations.DESCRIPTION]). + * + * In case of a conflict, the [conflictResolution] strategy is applied: + * - [ConflictResolutionStrategy.MERGE]: Merges the type and object properties, giving precedence to type properties. + * - [ConflictResolutionStrategy.OBJECT_ONLY] (default): Uses only the object properties. + * + * Finally, permissions are computed and the featured relation view is returned. + * + * @param objectId The object's ID. + * @param blocks The list of blocks; the featured relations block is used. + * @param urlBuilder Used for URL generation in views. + * @param fieldParser Parses fields for view rendering. + * @param storeOfObjectTypes Store for object type information. + * @param storeOfRelations Store for relation properties. + * @param details Provides object view context. + * @param participantCanEdit Indicates if the participant has edit permissions. + * @param conflictResolution Determines which strategy to use when a conflict is detected. + * + * @return The [BlockView.FeaturedRelation] view, or `null` if no valid featured block or object is found. + */ +suspend fun toFeaturedPropertiesViews( + objectId: Id, + blocks: List, + urlBuilder: UrlBuilder, + fieldParser: FieldParser, + storeOfObjectTypes: StoreOfObjectTypes, + storeOfRelations: StoreOfRelations, + details: ObjectViewDetails, + participantCanEdit: Boolean, + conflictResolutionStrategy: ConflictResolutionStrategy = ConflictResolutionStrategy.OBJECT_ONLY +): BlockView.FeaturedRelation? { + + val block = blocks.find { it.content is Block.Content.FeaturedRelations } + + if (block != null) { + val views = mutableListOf() + val currentObject = details.getObject(objectId) + if (currentObject?.isValid != true) { + //object not found or not valid, do not render featured properties + return null + } + val objectFeaturedProperties = storeOfRelations.getByKeys( + keys = currentObject.featuredRelations + ) + + val currType = details.getTypeForObject(objectId) + // Determine the effective object type. If the type is TEMPLATE, use the target object type. + val effectiveType = if (currType?.uniqueKey == ObjectTypeIds.TEMPLATE) { + currentObject.targetObjectType?.let { storeOfObjectTypes.get(it) } + } else { + currType + } + + val typeRecommendedFeaturedPropertiesIds = if (effectiveType != null) { + // Parse the object's properties using the effective type. + val parsedProperties = fieldParser.getObjectParsedProperties( + objectType = effectiveType, + objPropertiesKeys = currentObject.map.keys.toList(), + storeOfRelations = storeOfRelations + ) + parsedProperties.header.map { it.id } + } else { + emptyList() + } + + val typeRecommendedFeaturedProperties = storeOfRelations.getById( + ids = typeRecommendedFeaturedPropertiesIds + ) + + val hasConflict = objectFeaturedProperties.any { property -> property.key != Relations.DESCRIPTION } == true + + if (!hasConflict) { + val featuredViews = typeRecommendedFeaturedProperties.mapNotNull { property -> + property.toView( + currentObject = currentObject, + typeOfCurrentObject = currType, + details = details, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes, + fieldParser = fieldParser, + storeOfRelations = storeOfRelations + ) + } + views.addAll(featuredViews) + } else { + when (conflictResolutionStrategy) { + ConflictResolutionStrategy.MERGE -> { + val displayPropertiesMap = LinkedHashMap() + for (prop in typeRecommendedFeaturedProperties) { + displayPropertiesMap[prop.id] = prop + } + for (prop in objectFeaturedProperties) { + displayPropertiesMap.putIfAbsent(prop.id, prop) + } + val featuredViews = displayPropertiesMap.values.mapNotNull { property -> + property.toView( + currentObject = currentObject, + typeOfCurrentObject = currType, + details = details, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes, + fieldParser = fieldParser, + storeOfRelations = storeOfRelations + ) + } + views.addAll(featuredViews) + } + ConflictResolutionStrategy.OBJECT_ONLY -> { + val featuredViews = objectFeaturedProperties.mapNotNull { property -> + property.toView( + currentObject = currentObject, + typeOfCurrentObject = currType, + details = details, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes, + fieldParser = fieldParser, + storeOfRelations = storeOfRelations + ) + } + views.addAll(featuredViews) + } + } + } + + val canChangeType = currType?.toObjectPermissionsForTypes(participantCanEdit)?.canChangeType == true + return BlockView.FeaturedRelation( + id = block.id, + relations = views, + allowChangingObjectType = canChangeType, + isTodoLayout = currType?.recommendedLayout == ObjectType.Layout.TODO, + hasFeaturePropertiesConflict = hasConflict + ) + } else { + //featured block not found + return null + } +} + +private suspend fun ObjectWrapper.Relation.toView( + currentObject: ObjectWrapper.Basic, + typeOfCurrentObject: ObjectWrapper.Type?, + details: ObjectViewDetails, + urlBuilder: UrlBuilder, + storeOfObjectTypes: StoreOfObjectTypes, + storeOfRelations: StoreOfRelations, + fieldParser: FieldParser +) : ObjectRelationView? { + val property = this + val propertyKey = property.key + return when (propertyKey) { + Relations.DESCRIPTION -> null + Relations.TYPE -> { + if (typeOfCurrentObject == null || typeOfCurrentObject.isDeleted == true) { + val id = currentObject.getProperType() + if (id == null) { + null + } else { + ObjectRelationView.ObjectType.Deleted( + id = id, + key = propertyKey, + featured = true, + readOnly = false, + system = false + ) + } + } else { + ObjectRelationView.ObjectType.Base( + id = id, + key = propertyKey, + name = fieldParser.getObjectName(typeOfCurrentObject), + featured = true, + readOnly = false, + type = typeOfCurrentObject.id, + system = uniqueKey?.isSystemKey() == true + ) + } + } + Relations.SET_OF -> { + + val source = currentObject.setOf.firstOrNull() + + val wrapper = if (source != null) { + details.getObject(source) + } else { + null + } + + val isValid = wrapper?.isValid == true + val isDeleted = wrapper?.isDeleted == true + val isReadOnly = wrapper?.relationReadonlyValue == true + + val sources = if (isValid && !isDeleted) { + listOf( + wrapper.toObjectViewDefault( + urlBuilder = urlBuilder, + fieldParser = fieldParser, + storeOfObjectTypes = storeOfObjectTypes + ) + ) + } else { + emptyList() + } + + ObjectRelationView.Source( + id = currentObject.id, + key = propertyKey, + name = Relations.RELATION_NAME_EMPTY, + featured = true, + readOnly = isReadOnly, + sources = sources, + system = propertyKey.isSystemKey() + ) + } + Relations.BACKLINKS, Relations.LINKS -> { + details.linksFeaturedRelation( + relations = storeOfRelations.getAll(), + ctx = currentObject.id, + relationKey = propertyKey, + isFeatured = true + ) + } + else -> { + property.view( + details = details, + values = currentObject.map, + urlBuilder = urlBuilder, + isFeatured = true, + fieldParser = fieldParser, + storeOfObjectTypes = storeOfObjectTypes + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectWrapperExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectWrapperExtensions.kt index 8903868f44..ea6c8df37d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectWrapperExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectWrapperExtensions.kt @@ -370,41 +370,4 @@ private fun updateObjectIcon(obj: ObjectView): ObjectView { is ObjectView.Default -> obj.copy(icon = ObjectIcon.None) is ObjectView.Deleted -> obj } -} - -/** - * Retrieves the list of featured header property IDs for the current [ObjectWrapper.Basic] object. - * - * All objects have an associated type which can be obtained from [StoreOfObjectTypes]. In the case that the object's - * type is a Template (i.e. its unique key equals [ObjectTypeIds.TEMPLATE]), the target object type is resolved using - * the [targetObjectType] property. Once the effective object type is determined, the function uses [FieldParser] to - * obtain parsed properties and returns the header IDs. - * - * @param storeOfObjectTypes The store that provides object types. - * @param storeOfRelations The store that provides relations between objects. - * @param fieldParser The parser used to extract parsed properties from an object. - * @return A list of header property IDs, or an empty list if the necessary object type is not found. - */ -suspend fun ObjectWrapper.Basic.getFeaturedPropertiesIds( - storeOfObjectTypes: StoreOfObjectTypes, - storeOfRelations: StoreOfRelations, - fieldParser: FieldParser, -): List { - // Retrieve the object's current type. - val currentObjType = storeOfObjectTypes.getTypeOfObject(this) ?: return emptyList() - - // Determine the effective object type. If the type is TEMPLATE, use the target object type. - val effectiveType = if (currentObjType.uniqueKey == ObjectTypeIds.TEMPLATE) { - this.targetObjectType?.let { storeOfObjectTypes.get(it) } ?: return emptyList() - } else { - currentObjType - } - - // Parse the object's properties using the effective type. - val parsedProperties = fieldParser.getObjectParsedProperties( - objectType = effectiveType, - objPropertiesKeys = this.map.keys.toList(), - storeOfRelations = storeOfRelations - ) - return parsedProperties.header.map { it.id } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationObjectExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationObjectExtensions.kt index bee95e194b..cbda51db3f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationObjectExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationObjectExtensions.kt @@ -12,7 +12,6 @@ import com.anytypeio.anytype.core_models.ObjectViewDetails import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.presentation.extension.getObject import com.anytypeio.anytype.presentation.extension.getOptionObject -import com.anytypeio.anytype.presentation.extension.getTypeObject import com.anytypeio.anytype.presentation.number.NumberParser import com.anytypeio.anytype.presentation.sets.buildFileViews import com.anytypeio.anytype.presentation.objects.buildRelationValueObjectViews @@ -224,33 +223,6 @@ fun tagRelation( ) } -fun ObjectViewDetails.objectTypeRelation( - relationKey: Key, - isFeatured: Boolean, - objectTypeId: Id -): ObjectRelationView { - val objectType = getTypeObject(objectTypeId) - return if (objectType == null || objectType.isDeleted == true) { - ObjectRelationView.ObjectType.Deleted( - id = objectTypeId, - key = relationKey, - featured = isFeatured, - readOnly = false, - system = relationKey.isSystemKey() - ) - } else { - ObjectRelationView.ObjectType.Base( - id = objectTypeId, - key = relationKey, - name = objectType.name.orEmpty(), - featured = isFeatured, - readOnly = false, - type = objectTypeId, - system = relationKey.isSystemKey() - ) - } -} - fun ObjectViewDetails.linksFeaturedRelation( relations: List, relationKey: Key, 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 036633a1ef..7368035c0c 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 @@ -1,6 +1,5 @@ package com.anytypeio.anytype.presentation.sets -import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.CoverType import com.anytypeio.anytype.core_models.DVFilter import com.anytypeio.anytype.core_models.DVFilterCondition @@ -38,22 +37,13 @@ import com.anytypeio.anytype.domain.misc.DateProvider 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.domain.primitives.FieldParser import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider -import com.anytypeio.anytype.core_models.ObjectViewDetails import com.anytypeio.anytype.presentation.extension.getObject import com.anytypeio.anytype.presentation.extension.getTypeObject -import com.anytypeio.anytype.presentation.editor.editor.model.BlockView -import com.anytypeio.anytype.presentation.objects.getProperType -import com.anytypeio.anytype.presentation.objects.toObjectViewDefault import com.anytypeio.anytype.presentation.relations.BasicObjectCoverWrapper -import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.relations.ObjectSetConfig.ID_KEY import com.anytypeio.anytype.presentation.relations.getCover -import com.anytypeio.anytype.presentation.relations.isSystemKey -import com.anytypeio.anytype.presentation.relations.linksFeaturedRelation import com.anytypeio.anytype.presentation.relations.title -import com.anytypeio.anytype.presentation.relations.view import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView import com.anytypeio.anytype.presentation.sets.model.Viewer import com.anytypeio.anytype.presentation.sets.state.ObjectState @@ -63,38 +53,6 @@ import com.anytypeio.anytype.presentation.sets.viewer.ViewerView import com.anytypeio.anytype.presentation.templates.TemplateView import timber.log.Timber -suspend fun ObjectState.DataView.featuredRelations( - ctx: Id, - urlBuilder: UrlBuilder, - relations: List, - fieldParser: FieldParser, - storeOfObjectTypes: StoreOfObjectTypes, -): BlockView.FeaturedRelation? { - val block = blocks.find { it.content is Block.Content.FeaturedRelations } - if (block != null) { - val views = mutableListOf() - val currentObject = details.getObject(ctx) - val ids = currentObject?.featuredRelations - views.addAll( - mapFeaturedRelations( - ctx = ctx, - keys = ids, - details = details, - relations = relations, - urlBuilder = urlBuilder, - fieldParser = fieldParser, - storeOfObjectTypes = storeOfObjectTypes - ) - ) - return BlockView.FeaturedRelation( - id = block.id, - relations = views - ) - } else { - return null - } -} - fun ObjectState.DataView.header( ctx: Id, urlBuilder: UrlBuilder, @@ -127,113 +85,6 @@ fun ObjectState.DataView.header( } } -private suspend fun ObjectState.DataView.mapFeaturedRelations( - ctx: Id, - keys: List?, - details: ObjectViewDetails, - relations: List, - urlBuilder: UrlBuilder, - fieldParser: FieldParser, - storeOfObjectTypes: StoreOfObjectTypes, -): List { - val currentObject = details.getObject(ctx) ?: return emptyList() - val featuredRelationsIds = currentObject.featuredRelations - return featuredRelationsIds.mapNotNull { key -> - when (key) { - Relations.DESCRIPTION -> null - Relations.TYPE -> { - val currentObjectType = currentObject.getProperType() - if (currentObjectType != null) { - val wrapper = details.getTypeObject(currentObjectType) - if (wrapper != null) { - val isDeleted = wrapper.isDeleted == true - if (isDeleted) { - ObjectRelationView.ObjectType.Deleted( - id = wrapper.id, - key = key, - featured = true, - readOnly = false, - system = key.isSystemKey() - ) - } else { - ObjectRelationView.ObjectType.Base( - id = wrapper.id, - key = key, - name = wrapper.name.orEmpty(), - featured = true, - readOnly = false, - type = wrapper.id, - system = key.isSystemKey() - ) - } - } else { - null - } - } else { - null - } - } - Relations.SET_OF -> { - - val source = currentObject.setOf.firstOrNull() - - val wrapper = if (source != null) { - details.getObject(source) - } else { - null - } - - val isValid = wrapper?.isValid == true - val isDeleted = wrapper?.isDeleted == true - val isReadOnly = wrapper?.relationReadonlyValue == true - - val sources = if (isValid && !isDeleted) { - listOf( - wrapper.toObjectViewDefault( - urlBuilder = urlBuilder, - fieldParser = fieldParser, - storeOfObjectTypes = storeOfObjectTypes - ) - ) - } else { - emptyList() - } - - ObjectRelationView.Source( - id = currentObject.id, - key = key, - name = Relations.RELATION_NAME_EMPTY, - featured = true, - readOnly = isReadOnly, - sources = sources, - system = key.isSystemKey() - ) - } - Relations.BACKLINKS, Relations.LINKS -> { - details.linksFeaturedRelation( - relations = relations, - ctx = ctx, - relationKey = key, - isFeatured = true - ) - } - else -> { - val relation = relations.firstOrNull { it.key == key } - relation?.view( - details = details, - values = currentObject.map, - urlBuilder = urlBuilder, - isFeatured = true, - fieldParser = fieldParser, - storeOfObjectTypes = storeOfObjectTypes - ) - } - } - } - .sortedByDescending { it.key == Relations.SET_OF } - .sortedByDescending { it.key == Relations.TYPE } -} - fun List.update(new: List): List { val update = new.associateBy { rec -> rec[ID_KEY] as String } val result = mutableListOf() 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 cbb418fd6f..083f15fd42 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 @@ -82,6 +82,7 @@ import com.anytypeio.anytype.presentation.navigation.leftButtonClickAnalytics import com.anytypeio.anytype.presentation.objects.getCreateObjectParams import com.anytypeio.anytype.presentation.objects.isCreateObjectAllowed import com.anytypeio.anytype.presentation.objects.isTemplatesAllowed +import com.anytypeio.anytype.presentation.objects.toFeaturedPropertiesViews import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.relations.ObjectSetConfig.DEFAULT_LIMIT import com.anytypeio.anytype.presentation.relations.RelationListViewModel @@ -244,12 +245,15 @@ class ObjectSetViewModel( state to permission } .collectLatest { (state, permission) -> - featured.value = state.featuredRelations( - ctx = vmParams.ctx, + featured.value = toFeaturedPropertiesViews( + objectId = vmParams.ctx, urlBuilder = urlBuilder, - relations = storeOfRelations.getAll(), fieldParser = fieldParser, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + storeOfRelations = storeOfRelations, + blocks = state.blocks, + details = state.details, + participantCanEdit = permission?.isOwnerOrEditor() == true ) _header.value = state.header( ctx = vmParams.ctx, diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorFeaturedRelationsTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorFeaturedRelationsTest.kt index 47791a0ed4..ac39100383 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorFeaturedRelationsTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorFeaturedRelationsTest.kt @@ -154,6 +154,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { ), BlockView.FeaturedRelation( id = featuredBlock.id, + hasFeaturePropertiesConflict = true, relations = listOf( ObjectRelationView.ObjectType.Base( id = objectTypeId, @@ -161,7 +162,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { name = objectTypeName, featured = true, type = objectTypeId, - system = true + system = false ), ObjectRelationView.Default( id = r3.id, @@ -195,7 +196,10 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { val first = test.awaitValue() val second = test.awaitValue() - second.assertValue(ViewState.Success(expected)) + assertEquals( + expected = ViewState.Success(expected), + actual = second.value() + ) } @Test @@ -492,6 +496,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { ), BlockView.FeaturedRelation( id = featuredBlock.id, + hasFeaturePropertiesConflict = true, relations = listOf( ObjectRelationView.ObjectType.Base( id = objectTypeId, @@ -499,7 +504,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { name = objectTypeName, featured = true, type = objectTypeId, - system = true + system = false ) ) ), @@ -622,6 +627,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { ), BlockView.FeaturedRelation( id = featuredBlock.id, + hasFeaturePropertiesConflict = true, relations = listOf( ObjectRelationView.ObjectType.Base( id = objectTypeId, @@ -629,7 +635,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { name = objectTypeName, featured = true, type = objectTypeId, - system = true + system = false ), ObjectRelationView.Default( id = r1.id, @@ -761,12 +767,13 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { ), BlockView.FeaturedRelation( id = featuredBlock.id, + hasFeaturePropertiesConflict = true, relations = listOf( ObjectRelationView.ObjectType.Deleted( id = objectTypeId, key = Relations.TYPE, featured = true, - system = true + system = false ), ObjectRelationView.Default( id = r3.id, @@ -893,12 +900,13 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { ), BlockView.FeaturedRelation( id = featuredBlock.id, + hasFeaturePropertiesConflict = true, relations = listOf( ObjectRelationView.ObjectType.Deleted( id = objectTypeId, key = Relations.TYPE, featured = true, - system = true + system = false ), ObjectRelationView.Default( id = r3.id, @@ -1023,6 +1031,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { ), BlockView.FeaturedRelation( id = featuredBlock.id, + hasFeaturePropertiesConflict = true, relations = listOf( ObjectRelationView.Links.Backlinks( id = backlinksRelation.id, @@ -1157,7 +1166,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { } @Test - fun `should use Featured Properties Ids from Object Type when object featured ids are empty `() = + fun `should use Featured Properties Ids from Object Type when Type is not the Template`() = runTest { val title = MockTypicalDocumentFactory.title @@ -1244,6 +1253,8 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { ), BlockView.FeaturedRelation( id = featuredBlock.id, + //no conflict, because object featured properties are empty + hasFeaturePropertiesConflict = false, relations = listOf( ObjectRelationView.Default( id = property1.id, @@ -1288,7 +1299,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { } @Test - fun `should use Featured Properties Ids from TargetObjectTypeId when object is Template and his FeatureRelations are empty`() = + fun `should use Recommended Featured Properties Ids from TargetObjectTypeId when object is Template`() = runTest { val title = MockTypicalDocumentFactory.title @@ -1336,7 +1347,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { id = MockDataFactory.randomString(), uniqueKey = ObjectTypeIds.TEMPLATE, layout = ObjectType.Layout.OBJECT_TYPE.code.toDouble(), - recommendedFeaturedRelations = listOf(property1.id, property2.id) + recommendedFeaturedRelations = listOf(property2.id) ) val targetObjectType = StubObjectType( @@ -1397,6 +1408,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { ), BlockView.FeaturedRelation( id = featuredBlock.id, + hasFeaturePropertiesConflict = false, relations = listOf( ObjectRelationView.Default( id = property3.id, diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorNoteLayoutTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorNoteLayoutTest.kt index cb2c117f53..82e145d2e7 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorNoteLayoutTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorNoteLayoutTest.kt @@ -131,6 +131,8 @@ class EditorNoteLayoutTest : EditorPresentationTestSetup() { val expected = listOf( BlockView.FeaturedRelation( id = featuredBlock.id, + hasFeaturePropertiesConflict = true, + allowChangingObjectType = false, relations = listOf( ObjectRelationView.Default( id = r1.id, @@ -231,6 +233,8 @@ class EditorNoteLayoutTest : EditorPresentationTestSetup() { val expected = listOf( BlockView.FeaturedRelation( id = featuredBlock.id, + allowChangingObjectType = false, + hasFeaturePropertiesConflict = true, relations = listOf( ObjectRelationView.Default( id = r1.id, diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt index c1533574c4..b30d9e2cda 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt @@ -25,6 +25,7 @@ import com.anytypeio.anytype.core_models.StubWidgetBlock import com.anytypeio.anytype.core_models.UNKNOWN_SPACE_TYPE import com.anytypeio.anytype.core_models.WidgetSession import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions +import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.primitives.TypeId import com.anytypeio.anytype.core_models.primitives.TypeKey diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/SetObjectFeaturedPropertiesTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/SetObjectFeaturedPropertiesTest.kt new file mode 100644 index 0000000000..2a72611ab6 --- /dev/null +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/SetObjectFeaturedPropertiesTest.kt @@ -0,0 +1,373 @@ +package com.anytypeio.anytype.presentation.sets + +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectTypeIds +import com.anytypeio.anytype.core_models.ObjectViewDetails +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.StubFeatured +import com.anytypeio.anytype.core_models.StubObject +import com.anytypeio.anytype.core_models.StubObjectType +import com.anytypeio.anytype.core_models.StubRelationObject +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.misc.DateProvider +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.objects.DefaultStoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations +import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp +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.primitives.FieldParserImpl +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.presentation.objects.ConflictResolutionStrategy +import com.anytypeio.anytype.presentation.objects.toFeaturedPropertiesViews +import com.anytypeio.anytype.presentation.sets.state.ObjectState +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.Before +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub + +class SetObjectFeaturedPropertiesTest { + + @Mock + lateinit var urlBuilder: UrlBuilder + + lateinit var fieldParser: FieldParser + + @Mock + lateinit var dateProvider: DateProvider + + @Mock + lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp + + @Mock + lateinit var stringResourceProvider: StringResourceProvider + + @Mock + lateinit var logger: Logger + + private val dispatcher = StandardTestDispatcher(TestCoroutineScheduler()) + + @OptIn(ExperimentalCoroutinesApi::class) + val dispatchers = AppCoroutineDispatchers( + io = dispatcher, + main = dispatcher, + computation = dispatcher + ).also { Dispatchers.setMain(dispatcher) } + + private val storeOfRelations: StoreOfRelations = DefaultStoreOfRelations() + private val storeOfObjectTypes: StoreOfObjectTypes = DefaultStoreOfObjectTypes() + + @Before + fun before() { + MockitoAnnotations.openMocks(this) + fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider) + urlBuilder.stub { + on { large(any()) } doReturn "any/url" + } + } + + @Test + fun `case when without conflict`() = runTest { + + val propertyObjectType = StubRelationObject( + id = "propertyObjectType_id", + name = "Object type", + ) + val propertyTag = StubRelationObject( + id = "propertyTag_id", + name = "Tag", + ) + val propertyBacklinks = StubRelationObject( + id = "propertyBacklinks_id", + name = "Backlinks", + ) + val propertyDescription = StubRelationObject( + id = "propertyDescription_id", + name = "Description", + key = Relations.DESCRIPTION + ) + + storeOfRelations.merge( + relations = listOf( + propertyObjectType, + propertyTag, + propertyBacklinks, + propertyDescription + ) + ) + + val objType = StubObjectType( + name = "Query", + uniqueKey = ObjectTypeIds.SET, + recommendedLayout = ObjectType.Layout.SET.code.toDouble(), + recommendedFeaturedRelations = listOf( + propertyObjectType.id, + propertyTag.id, + propertyBacklinks.id, + propertyDescription.id + ) + ) + + storeOfObjectTypes.merge( + types = listOf(objType) + ) + + val objectSet = StubObject( + id = "id", + name = "Pages", + description = "This the description of Pages Set", + objectType = objType.id, + extraFields = mapOf( + Relations.FEATURED_RELATIONS to propertyDescription.key + ) + ) + + val featuredBlock = StubFeatured() + + val objectState = ObjectState.DataView.Set( + root = objectSet.id, + blocks = listOf(featuredBlock), + details = ObjectViewDetails( + details = mapOf( + objectSet.id to objectSet.map, + objType.id to objType.map, + ) + ) + ) + + val featuredPropertiesBlock = toFeaturedPropertiesViews( + objectId = objectSet.id, + storeOfRelations = storeOfRelations, + storeOfObjectTypes = storeOfObjectTypes, + urlBuilder = urlBuilder, + fieldParser = fieldParser, + details = objectState.details, + blocks = objectState.blocks, + participantCanEdit = true + ) + + assertEquals( + expected = listOf(propertyObjectType.key, propertyTag.key, propertyBacklinks.key), + actual = featuredPropertiesBlock!!.relations.map { it.key }) + } + + @Test + fun `case when with conflict, using default Strategy - OBJECT_ONLY`() = runTest { + + val propertyObjectType = StubRelationObject( + id = "propertyObjectType_id", + name = "Object type", + key = Relations.TYPE + ) + val propertyTag = StubRelationObject( + id = "propertyTag_id", + name = "Tag", + key = "key-tag" + ) + val propertyBacklinks = StubRelationObject( + id = "propertyBacklinks_id", + name = "Backlinks", + key = "key-backlinks" + ) + val propertyDescription = StubRelationObject( + id = "propertyDescription_id", + name = "Description", + key = Relations.DESCRIPTION + ) + + val propertyAuthor = StubRelationObject( + id = "propertyAuthor_id", + name = "Author", + key = "key-author" + ) + + storeOfRelations.merge( + relations = listOf( + propertyObjectType, + propertyTag, + propertyBacklinks, + propertyDescription, + propertyAuthor + ) + ) + + val objType = StubObjectType( + name = "Query", + uniqueKey = ObjectTypeIds.SET, + recommendedLayout = ObjectType.Layout.SET.code.toDouble(), + recommendedFeaturedRelations = listOf( + propertyObjectType.id, + propertyTag.id, + propertyBacklinks.id, + propertyDescription.id + ) + ) + + storeOfObjectTypes.merge( + types = listOf(objType) + ) + + val objectSet = StubObject( + id = "id", + name = "Pages", + description = "This the description of Pages Set", + objectType = objType.id, + extraFields = mapOf( + Relations.FEATURED_RELATIONS to listOf( + propertyBacklinks.key, + propertyAuthor.key, + propertyDescription.key + ) + ) + ) + + val featuredBlock = StubFeatured() + + val objectState = ObjectState.DataView.Set( + root = objectSet.id, + blocks = listOf(featuredBlock), + details = ObjectViewDetails( + details = mapOf( + objectSet.id to objectSet.map, + objType.id to objType.map, + ) + ) + ) + + val featuredPropertiesBlock = toFeaturedPropertiesViews( + objectId = objectSet.id, + storeOfRelations = storeOfRelations, + storeOfObjectTypes = storeOfObjectTypes, + urlBuilder = urlBuilder, + fieldParser = fieldParser, + details = objectState.details, + blocks = objectState.blocks, + participantCanEdit = true + ) + + assertEquals( + expected = listOf( + propertyBacklinks.key, + propertyAuthor.key + ), + actual = featuredPropertiesBlock!!.relations.map { it.key }) + } + + @Test + fun `case when with conflict, using Strategy - MERGE`() = runTest { + + val propertyObjectType = StubRelationObject( + id = "propertyObjectType_id", + name = "Object type", + key = Relations.TYPE + ) + val propertyTag = StubRelationObject( + id = "propertyTag_id", + name = "Tag", + key = "key-tag" + ) + val propertyBacklinks = StubRelationObject( + id = "propertyBacklinks_id", + name = "Backlinks", + key = "key-backlinks" + ) + val propertyDescription = StubRelationObject( + id = "propertyDescription_id", + name = "Description", + key = Relations.DESCRIPTION + ) + + val propertyAuthor = StubRelationObject( + id = "propertyAuthor_id", + name = "Author", + key = "key-author" + ) + + storeOfRelations.merge( + relations = listOf( + propertyObjectType, + propertyTag, + propertyBacklinks, + propertyDescription, + propertyAuthor + ) + ) + + val objType = StubObjectType( + name = "Query", + uniqueKey = ObjectTypeIds.SET, + recommendedLayout = ObjectType.Layout.SET.code.toDouble(), + recommendedFeaturedRelations = listOf( + propertyObjectType.id, + propertyTag.id, + propertyBacklinks.id, + propertyDescription.id + ) + ) + + storeOfObjectTypes.merge( + types = listOf(objType) + ) + + val objectSet = StubObject( + id = "id", + name = "Pages", + description = "This the description of Pages Set", + objectType = objType.id, + extraFields = mapOf( + Relations.FEATURED_RELATIONS to listOf( + propertyBacklinks.key, + propertyAuthor.key, + propertyDescription.key + ) + ) + ) + + val featuredBlock = StubFeatured() + + val objectState = ObjectState.DataView.Set( + root = objectSet.id, + blocks = listOf(featuredBlock), + details = ObjectViewDetails( + details = mapOf( + objectSet.id to objectSet.map, + objType.id to objType.map, + ) + ) + ) + + val featuredPropertiesBlock = toFeaturedPropertiesViews( + objectId = objectSet.id, + storeOfRelations = storeOfRelations, + storeOfObjectTypes = storeOfObjectTypes, + urlBuilder = urlBuilder, + fieldParser = fieldParser, + details = objectState.details, + blocks = objectState.blocks, + participantCanEdit = true, + conflictResolutionStrategy = ConflictResolutionStrategy.MERGE + ) + + assertEquals( + expected = listOf( + propertyObjectType.key, + propertyTag.key, + propertyBacklinks.key, + propertyAuthor.key + ), + actual = featuredPropertiesBlock!!.relations.map { it.key }) + } +} \ No newline at end of file