1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3399 Primitives | Object's featured properties (#2211)

This commit is contained in:
Konstantin Ivanov 2025-03-31 15:20:01 +02:00 committed by GitHub
parent 3bf0c305a0
commit bef75d1cbf
Signed by: github
GPG key ID: B5690EEEBB952194
13 changed files with 691 additions and 321 deletions

View file

@ -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) }

View file

@ -1287,8 +1287,9 @@ sealed class BlockView : ViewType {
data class FeaturedRelation(
override val id: String,
val relations: List<ObjectRelationView>,
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
}

View file

@ -33,6 +33,7 @@ interface BlockViewRenderer {
selection: Set<Id>,
count: Int = 0,
parentScheme: NestedDecorationData = emptyList(),
participantCanEdit: Boolean = false,
onRenderFlag: (RenderFlag) -> Unit = {},
): List<BlockView>

View file

@ -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<Id>,
count: Int,
parentScheme: NestedDecorationData,
participantCanEdit: Boolean,
onRenderFlag: (BlockViewRenderer.RenderFlag) -> Unit,
): List<BlockView> {
@ -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<Key>,
values: Map<String, Any?>
@ -2116,68 +2097,6 @@ class DefaultBlockViewRenderer @Inject constructor(
return result
}
private suspend fun mapFeaturedRelations(
ctx: Id,
currentObject: ObjectWrapper.Basic,
details: ObjectViewDetails,
): List<ObjectRelationView> {
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,

View file

@ -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<Block>,
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<ObjectRelationView>()
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<String, ObjectWrapper.Relation>()
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
)
}
}
}

View file

@ -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<Id> {
// 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 }
}

View file

@ -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<ObjectWrapper.Relation>,
relationKey: Key,

View file

@ -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<ObjectWrapper.Relation>,
fieldParser: FieldParser,
storeOfObjectTypes: StoreOfObjectTypes,
): BlockView.FeaturedRelation? {
val block = blocks.find { it.content is Block.Content.FeaturedRelations }
if (block != null) {
val views = mutableListOf<ObjectRelationView>()
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<String>?,
details: ObjectViewDetails,
relations: List<ObjectWrapper.Relation>,
urlBuilder: UrlBuilder,
fieldParser: FieldParser,
storeOfObjectTypes: StoreOfObjectTypes,
): List<ObjectRelationView> {
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<DVRecord>.update(new: List<DVRecord>): List<DVRecord> {
val update = new.associateBy { rec -> rec[ID_KEY] as String }
val result = mutableListOf<DVRecord>()

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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 })
}
}