diff --git a/core/block/editor.go b/core/block/editor.go index 3d064e29db..db35dc190c 100644 --- a/core/block/editor.go +++ b/core/block/editor.go @@ -615,16 +615,8 @@ func (s *Service) GetRelations(ctx session.Context, objectId string) (relations // ModifyDetails performs details get and update under the sb lock to make sure no modifications are done in the middle func (s *Service) ModifyDetails(objectId string, modifier func(current *types.Struct) (*types.Struct, error)) (err error) { - if modifier == nil { - return fmt.Errorf("modifier is nil") - } - return Do(s, objectId, func(b smartblock.SmartBlock) error { - dets, err := modifier(b.CombinedDetails()) - if err != nil { - return err - } - - return b.Apply(b.NewState().SetDetails(dets)) + return Do(s, objectId, func(du basic.DetailsUpdatable) error { + return du.UpdateDetails(modifier) }) } diff --git a/core/block/editor/basic/basic.go b/core/block/editor/basic/basic.go index d2422ffeab..642b4915c6 100644 --- a/core/block/editor/basic/basic.go +++ b/core/block/editor/basic/basic.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/globalsign/mgo/bson" + "github.com/gogo/protobuf/types" "github.com/samber/lo" "github.com/anyproto/anytype-heart/core/block/editor/converter" @@ -40,6 +41,7 @@ type AllOperations interface { type CommonOperations interface { DetailsSettable + DetailsUpdatable SetFields(ctx session.Context, fields ...*pb.RpcBlockListSetFieldsRequestBlockField) (err error) SetDivStyle(ctx session.Context, style model.BlockContentDivStyle, ids ...string) (err error) @@ -64,6 +66,10 @@ type DetailsSettable interface { SetDetails(ctx session.Context, details []*pb.RpcObjectSetDetailsDetail, showEvent bool) (err error) } +type DetailsUpdatable interface { + UpdateDetails(update func(current *types.Struct) (*types.Struct, error)) (err error) +} + type Restrictionable interface { Restrictions() restriction.Restrictions } @@ -402,7 +408,7 @@ func (bs *basic) FeaturedRelationAdd(ctx session.Context, relations ...string) ( } frc = append(frc, r) if !bs.HasRelation(s, r) { - err = bs.addRelationLink(r, s) + err = bs.addRelationLink(s, r) if err != nil { return fmt.Errorf("failed to add relation link on adding featured relation '%s': %w", r, err) } diff --git a/core/block/editor/basic/details.go b/core/block/editor/basic/details.go index 851ad5e7af..8660fe3c6f 100644 --- a/core/block/editor/basic/details.go +++ b/core/block/editor/basic/details.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/gogo/protobuf/types" + "golang.org/x/exp/maps" "github.com/anyproto/anytype-heart/core/block/editor/objecttype" "github.com/anyproto/anytype-heart/core/block/editor/smartblock" @@ -52,6 +53,25 @@ func (bs *basic) SetDetails(ctx session.Context, details []*pb.RpcObjectSetDetai return nil } +func (bs *basic) UpdateDetails(update func(current *types.Struct) (*types.Struct, error)) (err error) { + if update == nil { + return fmt.Errorf("update function is nil") + } + s := bs.NewState() + + newDetails, err := update(s.CombinedDetails()) + if err != nil { + return + } + s.SetDetails(newDetails) + + if err = bs.addRelationLinks(s, maps.Keys(newDetails.Fields)...); err != nil { + return + } + + return bs.Apply(s) +} + func (bs *basic) collectDetailUpdates(details []*pb.RpcObjectSetDetailsDetail, s *state.State) []*detailUpdate { updates := make([]*detailUpdate, 0, len(details)) for _, detail := range details { @@ -90,7 +110,7 @@ func (bs *basic) createDetailUpdate(st *state.State, detail *pb.RpcObjectSetDeta if err := bs.setDetailSpecialCases(st, detail); err != nil { return nil, fmt.Errorf("special case: %w", err) } - if err := bs.addRelationLink(detail.Key, st); err != nil { + if err := bs.addRelationLink(st, detail.Key); err != nil { return nil, err } if err := bs.validateDetailFormat(bs.SpaceID(), detail.Key, detail.Value); err != nil { @@ -246,7 +266,7 @@ func (bs *basic) setDetailSpecialCases(st *state.State, detail *pb.RpcObjectSetD return nil } -func (bs *basic) addRelationLink(relationKey string, st *state.State) error { +func (bs *basic) addRelationLink(st *state.State, relationKey string) error { relLink, err := bs.objectStore.GetRelationLink(bs.SpaceID(), relationKey) if err != nil || relLink == nil { return fmt.Errorf("failed to get relation: %w", err) @@ -255,6 +275,18 @@ func (bs *basic) addRelationLink(relationKey string, st *state.State) error { return nil } +func (bs *basic) addRelationLinks(st *state.State, relationKeys ...string) error { + if len(relationKeys) == 0 { + return nil + } + relations, err := bs.objectStore.FetchRelationByKeys(bs.SpaceID(), relationKeys...) + if err != nil || relations == nil { + return fmt.Errorf("failed to get relations: %w", err) + } + st.AddRelationLinks(relations.RelationLinks()...) + return nil +} + func (bs *basic) discardOwnSetDetailsEvent(ctx session.Context, showEvent bool) { if !showEvent && ctx != nil { var filtered []*pb.EventMessage diff --git a/core/block/editor/basic/details_test.go b/core/block/editor/basic/details_test.go new file mode 100644 index 0000000000..632308e327 --- /dev/null +++ b/core/block/editor/basic/details_test.go @@ -0,0 +1,138 @@ +package basic + +import ( + "testing" + + "github.com/gogo/protobuf/types" + "github.com/stretchr/testify/assert" + + "github.com/anyproto/anytype-heart/core/block/editor/converter" + "github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest" + "github.com/anyproto/anytype-heart/pb" + "github.com/anyproto/anytype-heart/pkg/lib/bundle" + "github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "github.com/anyproto/anytype-heart/util/pbtypes" +) + +type duFixture struct { + sb *smarttest.SmartTest + store *objectstore.StoreFixture + basic DetailsUpdatable +} + +var ( + objectId = "objectId" + spaceId = "space1" +) + +func newDUFixture(t *testing.T) *duFixture { + sb := smarttest.New(objectId) + sb.SetDetails(nil, nil, false) + sb.SetSpaceId(spaceId) + + store := objectstore.NewStoreFixture(t) + + b := NewBasic(sb, store, converter.NewLayoutConverter()) + + return &duFixture{ + sb: sb, + store: store, + basic: b, + } +} + +func TestBasic_UpdateDetails(t *testing.T) { + t.Run("add new details", func(t *testing.T) { + // given + f := newDUFixture(t) + f.store.AddObjects(t, []objectstore.TestObject{{ + bundle.RelationKeyId: pbtypes.String("rel-aperture"), + bundle.RelationKeySpaceId: pbtypes.String(spaceId), + bundle.RelationKeyRelationKey: pbtypes.String("aperture"), + bundle.RelationKeyUniqueKey: pbtypes.String("rel-aperture"), + bundle.RelationKeyRelationFormat: pbtypes.Int64(int64(model.RelationFormat_longtext)), + }, { + bundle.RelationKeyId: pbtypes.String("rel-maxCount"), + bundle.RelationKeySpaceId: pbtypes.String(spaceId), + bundle.RelationKeyRelationKey: pbtypes.String("relationMaxCount"), + bundle.RelationKeyUniqueKey: pbtypes.String("rel-relationMaxCount"), + bundle.RelationKeyRelationFormat: pbtypes.Int64(int64(model.RelationFormat_number)), + }}) + + // when + err := f.basic.UpdateDetails(func(current *types.Struct) (*types.Struct, error) { + current.Fields[bundle.RelationKeyAperture.String()] = pbtypes.String("aperture") + current.Fields[bundle.RelationKeyRelationMaxCount.String()] = pbtypes.Int64(5) + return current, nil + }) + + // then + assert.NoError(t, err) + + value, found := f.sb.Details().Fields[bundle.RelationKeyAperture.String()] + assert.True(t, found) + assert.Equal(t, pbtypes.String("aperture"), value) + assert.True(t, f.sb.HasRelation(f.sb.NewState(), bundle.RelationKeyAperture.String())) + + value, found = f.sb.Details().Fields[bundle.RelationKeyRelationMaxCount.String()] + assert.True(t, found) + assert.Equal(t, pbtypes.Int64(5), value) + assert.True(t, f.sb.HasRelation(f.sb.NewState(), bundle.RelationKeyRelationMaxCount.String())) + }) + + t.Run("modify details", func(t *testing.T) { + // given + f := newDUFixture(t) + err := f.sb.SetDetails(nil, []*pb.RpcObjectSetDetailsDetail{{ + Key: bundle.RelationKeySpaceDashboardId.String(), + Value: pbtypes.String("123"), + }}, false) + assert.NoError(t, err) + f.store.AddObjects(t, []objectstore.TestObject{{ + bundle.RelationKeyId: pbtypes.String("rel-spaceDashboardId"), + bundle.RelationKeySpaceId: pbtypes.String(spaceId), + bundle.RelationKeyRelationKey: pbtypes.String("spaceDashboardId"), + bundle.RelationKeyUniqueKey: pbtypes.String("rel-spaceDashboardId"), + bundle.RelationKeyRelationFormat: pbtypes.Int64(int64(model.RelationFormat_object)), + }}) + + // when + err = f.basic.UpdateDetails(func(current *types.Struct) (*types.Struct, error) { + current.Fields[bundle.RelationKeySpaceDashboardId.String()] = pbtypes.String("new123") + return current, nil + }) + + // then + assert.NoError(t, err) + + value, found := f.sb.Details().Fields[bundle.RelationKeySpaceDashboardId.String()] + assert.True(t, found) + assert.Equal(t, pbtypes.String("new123"), value) + assert.True(t, f.sb.HasRelation(f.sb.NewState(), bundle.RelationKeySpaceDashboardId.String())) + }) + + t.Run("delete details", func(t *testing.T) { + // given + f := newDUFixture(t) + err := f.sb.SetDetails(nil, []*pb.RpcObjectSetDetailsDetail{{ + Key: bundle.RelationKeyTargetObjectType.String(), + Value: pbtypes.String("ot-note"), + }}, false) + assert.NoError(t, err) + + // when + err = f.basic.UpdateDetails(func(current *types.Struct) (*types.Struct, error) { + delete(current.Fields, bundle.RelationKeyTargetObjectType.String()) + return current, nil + }) + + // then + assert.NoError(t, err) + + value, found := f.sb.Details().Fields[bundle.RelationKeyTargetObjectType.String()] + assert.False(t, found) + assert.Nil(t, value) + assert.False(t, f.sb.HasRelation(f.sb.NewState(), bundle.RelationKeyTargetObjectType.String())) + }) +} diff --git a/core/block/editor/participant.go b/core/block/editor/participant.go index 550c86e233..78b55d7113 100644 --- a/core/block/editor/participant.go +++ b/core/block/editor/participant.go @@ -3,6 +3,7 @@ package editor import ( "time" + "github.com/anyproto/anytype-heart/core/block/editor/basic" "github.com/anyproto/anytype-heart/core/block/editor/smartblock" "github.com/anyproto/anytype-heart/core/block/editor/template" "github.com/anyproto/anytype-heart/pkg/lib/bundle" @@ -12,11 +13,14 @@ import ( type participant struct { smartblock.SmartBlock + basic.DetailsUpdatable } func (f *ObjectFactory) newParticipant(sb smartblock.SmartBlock) *participant { + basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter) return &participant{ - SmartBlock: sb, + SmartBlock: sb, + DetailsUpdatable: basicComponent, } } diff --git a/pkg/lib/bundle/relation.gen.go b/pkg/lib/bundle/relation.gen.go index 2cf25e3f3c..fea5cb6eaa 100644 --- a/pkg/lib/bundle/relation.gen.go +++ b/pkg/lib/bundle/relation.gen.go @@ -9,7 +9,7 @@ import ( "github.com/anyproto/anytype-heart/pkg/lib/pb/model" ) -const RelationChecksum = "d9166ff25c421f94e3fd6b640fa6ad8652b0d4a676e9963ffddb447d9a20e2ae" +const RelationChecksum = "d56fa782bf2d25bfc0e8e83717c349d63e49d1c3e96c6e83a342b1383942ffbf" const ( RelationKeyTag domain.RelationKey = "tag" RelationKeyCamera domain.RelationKey = "camera" @@ -859,7 +859,7 @@ var ( }, RelationKeyGlobalName: { - DataSource: model.Relation_details, + DataSource: model.Relation_derived, Description: "Name of profile that the user could be mentioned by", Format: model.RelationFormat_shorttext, Id: "_brglobalName", diff --git a/pkg/lib/bundle/relations.json b/pkg/lib/bundle/relations.json index 32fb762fb2..d822d74e0f 100644 --- a/pkg/lib/bundle/relations.json +++ b/pkg/lib/bundle/relations.json @@ -1697,6 +1697,6 @@ "maxCount": 1, "name": "Global name", "readonly": true, - "source": "details" + "source": "derived" } ]