diff --git a/gno/p/dao_core/dao_core.gno b/gno/p/dao_core/dao_core.gno index 2f83fca06f..86f9d7126d 100644 --- a/gno/p/dao_core/dao_core.gno +++ b/gno/p/dao_core/dao_core.gno @@ -48,16 +48,17 @@ func NewDAOCore( proposalModules: make([]dao_interfaces.ActivableProposalModule, len(proposalModulesFactories)), } - core.votingModule = votingModuleFactory(core) - if core.votingModule == nil { - panic("voting module factory returned nil") - } - + // important to keep this order since voting module might depend on roles module core.rolesModule = rolesModuleFactory(core) if core.rolesModule == nil { panic("roles module factory returned nil") } + core.votingModule = votingModuleFactory(core) + if core.votingModule == nil { + panic("voting module factory returned nil") + } + for i, modFactory := range proposalModulesFactories { mod := modFactory(core) if mod == nil { @@ -157,7 +158,7 @@ func (d *daoCore) GetMembersJSON(start, end string, limit uint64, height int64) } func (d *daoCore) VotingPowerAtHeight(address std.Address, height int64) uint64 { - return d.VotingModule().VotingPowerAtHeight(address, height) + return d.VotingModule().VotingPowerAtHeight(address, height, []string{}) } func (d *daoCore) ActiveProposalModuleCount() int { diff --git a/gno/p/dao_core/dao_core_test.gno b/gno/p/dao_core/dao_core_test.gno index 4a879aacdd..2c1013d155 100644 --- a/gno/p/dao_core/dao_core_test.gno +++ b/gno/p/dao_core/dao_core_test.gno @@ -38,7 +38,7 @@ func (vm *votingModule) Render(path string) string { return "# Test Voting Module" } -func (vm *votingModule) VotingPowerAtHeight(address std.Address, height int64) uint64 { +func (vm *votingModule) VotingPowerAtHeight(address std.Address, height int64, resources []string) uint64 { return 0 } @@ -73,7 +73,7 @@ func (rm *rolesModule) HasRole(address std.Address, role string) bool { return false } -func (rm *rolesModule) NewRole(roleName string) { +func (rm *rolesModule) NewRoleJSON(roleName, resourcesJSON string) { panic("not implemented") } @@ -93,6 +93,10 @@ func (rm *rolesModule) GetMemberRoles(address std.Address) []string { return []string{} } +func (rm *rolesModule) GetMemberResourceVPower(address std.Address, resource string) uint64 { + return 0 +} + type proposalModule struct { core dao_interfaces.IDAOCore } diff --git a/gno/p/dao_interfaces/modules.gno b/gno/p/dao_interfaces/modules.gno index d5b4d7c3a5..5565d9537a 100644 --- a/gno/p/dao_interfaces/modules.gno +++ b/gno/p/dao_interfaces/modules.gno @@ -18,7 +18,7 @@ type IVotingModule interface { ConfigJSON() string GetMembersJSON(start, end string, limit uint64, height int64) string Render(path string) string - VotingPowerAtHeight(address std.Address, height int64) (power uint64) + VotingPowerAtHeight(address std.Address, height int64, resources []string) (power uint64) TotalPowerAtHeight(height int64) uint64 } @@ -43,8 +43,9 @@ type IRolesModule interface { ConfigJSON() string Render(path string) string GetMemberRoles(address std.Address) []string + GetMemberResourceVPower(address std.Address, resource string) uint64 HasRole(address std.Address, role string) bool - NewRole(roleName string) + NewRoleJSON(roleName, resourcesJSON string) DeleteRole(roleName string) GrantRole(address std.Address, role string) RevokeRole(address std.Address, role string) diff --git a/gno/p/dao_proposal_single/dao_proposal_single.gno b/gno/p/dao_proposal_single/dao_proposal_single.gno index d95d33cf00..19f68130ec 100644 --- a/gno/p/dao_proposal_single/dao_proposal_single.gno +++ b/gno/p/dao_proposal_single/dao_proposal_single.gno @@ -331,7 +331,12 @@ func (d *DAOProposalSingle) VoteJSON(proposalID int, voteJSON string) { panic("proposal is expired") } - votePower := d.core.VotingModule().VotingPowerAtHeight(voter, proposal.StartHeight) + resources := make([]string, len(proposal.Messages)) + for i, m := range proposal.Messages { + resources[i] = m.Type() + } + + votePower := d.core.VotingModule().VotingPowerAtHeight(voter, proposal.StartHeight, resources) if votePower == 0 { panic("not registered") } diff --git a/gno/p/dao_roles_group/roles_group.gno b/gno/p/dao_roles_group/roles_group.gno index e10f45a40f..c7d81ece48 100644 --- a/gno/p/dao_roles_group/roles_group.gno +++ b/gno/p/dao_roles_group/roles_group.gno @@ -3,6 +3,7 @@ package dao_roles_group import ( "std" + "gno.land/p/demo/avl" "gno.land/p/demo/json" dao_interfaces "gno.land/p/teritori/dao_interfaces" "gno.land/p/teritori/jsonutil" @@ -12,12 +13,19 @@ import ( type RolesGroup struct { dao_interfaces.IRolesModule - rm *role_manager.RoleManager + rm *role_manager.RoleManager + resourcesVPower *avl.Tree // roles -> ResourceVPower[] +} + +type ResourceVPower struct { + Resource string + Power uint64 } func NewRolesGroup() *RolesGroup { return &RolesGroup{ - rm: role_manager.NewWithAddress(std.PrevRealm().Addr()), + rm: role_manager.NewWithAddress(std.PrevRealm().Addr()), + resourcesVPower: avl.NewTree(), } } @@ -42,8 +50,25 @@ func (r *RolesGroup) HasRole(address std.Address, role string) bool { return r.rm.HasRole(address, role) } -func (r *RolesGroup) NewRole(roleName string) { +func (r *RolesGroup) NewRoleJSON(roleName, resourcesJSON string) { + node := json.Must(json.Unmarshal([]byte(resourcesJSON))) + arr := node.MustArray() + resources := make([]ResourceVPower, len(arr)) + for i, n := range arr { + node := n.MustObject() + resources[i] = ResourceVPower{ + Resource: node["resource"].MustString(), + Power: jsonutil.MustUint64(node["power"]), + } + } + r.NewRole(roleName, resources) +} + +func (r *RolesGroup) NewRole(roleName string, resources []ResourceVPower) { r.rm.CreateNewRole(roleName, []string{}) + if len(resources) > 0 { + r.resourcesVPower.Set(roleName, resources) + } } func (r *RolesGroup) DeleteRole(roleName string) { @@ -61,3 +86,21 @@ func (r *RolesGroup) RevokeRole(address std.Address, role string) { func (r *RolesGroup) GetMemberRoles(address std.Address) []string { return r.rm.GetUserRoles(address) } + +func (r *RolesGroup) GetMemberResourceVPower(address std.Address, resource string) uint64 { + roles := r.rm.GetUserRoles(address) + power := uint64(0) + for _, role := range roles { + resourcesRaw, exists := r.resourcesVPower.Get(role) + if !exists { + continue + } + resources := resourcesRaw.([]ResourceVPower) + for _, r := range resources { + if r.Resource == resource && r.Power > power { + power = r.Power + } + } + } + return power +} diff --git a/gno/p/dao_roles_voting_group/gno.mod b/gno/p/dao_roles_voting_group/gno.mod new file mode 100644 index 0000000000..f782e5d8be --- /dev/null +++ b/gno/p/dao_roles_voting_group/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/dao_roles_voting_group diff --git a/gno/p/dao_roles_voting_group/messages.gno b/gno/p/dao_roles_voting_group/messages.gno new file mode 100644 index 0000000000..39b5874b3f --- /dev/null +++ b/gno/p/dao_roles_voting_group/messages.gno @@ -0,0 +1,62 @@ +package dao_roles_voting_group + +import ( + "gno.land/p/demo/json" + "gno.land/p/teritori/dao_interfaces" +) + +const updateMembersType = "gno.land/p/teritori/dao_voting_group.UpdateMembers" + +type UpdateMembersExecutableMessage []Member + +var _ dao_interfaces.ExecutableMessage = (*UpdateMembersExecutableMessage)(nil) + +func (m *UpdateMembersExecutableMessage) FromJSON(ast *json.Node) { + changes := ast.MustArray() + *m = make([]Member, len(changes)) + for i, change := range changes { + (*m)[i].FromJSON(change) + } +} + +func (m *UpdateMembersExecutableMessage) ToJSON() *json.Node { + changes := make([]*json.Node, len(*m)) + for i, change := range *m { + changes[i] = change.ToJSON() + } + + return json.ArrayNode("", changes) +} + +func (m *UpdateMembersExecutableMessage) String() string { + return m.ToJSON().String() +} + +func (m *UpdateMembersExecutableMessage) Type() string { + return updateMembersType +} + +type updateMembersHandler struct { + vg *RolesVotingGroup +} + +var _ dao_interfaces.MessageHandler = (*updateMembersHandler)(nil) + +func (h *updateMembersHandler) Type() string { + return updateMembersType +} + +func (h *updateMembersHandler) Execute(msg dao_interfaces.ExecutableMessage) { + m, ok := msg.(*UpdateMembersExecutableMessage) + if !ok { + panic("unexpected message type") + } + + for _, change := range *m { + h.vg.SetMemberPower(change.Address, change.Power) + } +} + +func (h *updateMembersHandler) Instantiate() dao_interfaces.ExecutableMessage { + return &UpdateMembersExecutableMessage{} +} diff --git a/gno/p/dao_roles_voting_group/roles_voting_group.gno b/gno/p/dao_roles_voting_group/roles_voting_group.gno new file mode 100644 index 0000000000..40f728cb19 --- /dev/null +++ b/gno/p/dao_roles_voting_group/roles_voting_group.gno @@ -0,0 +1,197 @@ +package dao_roles_voting_group + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/json" + dao_interfaces "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/havl" + "gno.land/p/teritori/jsonutil" +) + +type Member struct { + Address std.Address + Power uint64 +} + +func (m Member) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "address": jsonutil.AddressNode(m.Address), + "power": jsonutil.Uint64Node(m.Power), + }) +} + +func (m *Member) FromJSON(ast *json.Node) { + obj := ast.MustObject() + m.Address = jsonutil.MustAddress(obj["address"]) + m.Power = jsonutil.MustUint64(obj["power"]) +} + +type RolesVotingGroup struct { + dao_interfaces.IVotingModule + + powerByAddr *havl.Tree // std.Address -> uint64 + totalPower *havl.Tree // "" -> uint64 + memberCount *havl.Tree // "" -> uint32 + rolesModule dao_interfaces.IRolesModule +} + +func NewRolesVotingGroup(rm dao_interfaces.IRolesModule) *RolesVotingGroup { + return &RolesVotingGroup{ + powerByAddr: havl.NewTree(), + totalPower: havl.NewTree(), + memberCount: havl.NewTree(), + rolesModule: rm, + } +} + +func (v *RolesVotingGroup) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "gno.land/p/teritori/dao_voting_group", + Version: "0.1.0", + } +} + +func (v *RolesVotingGroup) ConfigJSON() string { + return json.ObjectNode("", map[string]*json.Node{ + "totalPower": jsonutil.Uint64Node(v.TotalPowerAtHeight(havl.Latest)), + "members": jsonutil.Uint32Node(v.MemberCount(havl.Latest)), + }).String() +} + +func (v *RolesVotingGroup) GetMembersJSON(start, end string, limit uint64, height int64) string { + members := v.GetMembers(start, end, limit, height) + membersJSON := make([]*json.Node, len(members)) + for i, m := range members { + membersJSON[i] = m.ToJSON() + } + return json.ArrayNode("", membersJSON).String() +} + +func (v *RolesVotingGroup) VotingPowerAtHeight(addr std.Address, height int64, resources []string) uint64 { + userPower, ok := v.powerByAddr.Get(addr.String(), height) + if !ok { + return 0 + } + + // In case there is many resources involved, we take the lowest value + rolePower := uint64(0) + for _, resource := range resources { + tmp := v.rolesModule.GetMemberResourceVPower(addr, resource) + if tmp < rolePower || rolePower == 0 { + rolePower = tmp + } + } + + if rolePower > userPower.(uint64) { + return rolePower + } + + return userPower.(uint64) +} + +func (v *RolesVotingGroup) TotalPowerAtHeight(height int64) uint64 { + p, ok := v.totalPower.Get("", height) + if !ok { + return 0 + } + + return p.(uint64) +} + +func (g *RolesVotingGroup) SetMemberPower(addr std.Address, power uint64) { + if power == 0 { + g.RemoveMember(addr) + return + } + + iprevious, ok := g.powerByAddr.Get(addr.String(), havl.Latest) + if !ok { + g.memberCount.Set("", g.MemberCount(havl.Latest)+1) + } + + previous := uint64(0) + if ok { + previous = iprevious.(uint64) + } + + if power == previous { + return + } + + g.powerByAddr.Set(addr.String(), power) + + ipreviousTotal, ok := g.totalPower.Get("", havl.Latest) + previousTotal := uint64(0) + if ok { + previousTotal = ipreviousTotal.(uint64) + } + + g.totalPower.Set("", (previousTotal+power)-previous) +} + +func (g *RolesVotingGroup) RemoveMember(addr std.Address) (uint64, bool) { + p, removed := g.powerByAddr.Remove(addr.String()) + if !removed { + return 0, false + } + + g.memberCount.Set("", g.MemberCount(havl.Latest)-1) + power := p.(uint64) + g.totalPower.Set("", g.TotalPowerAtHeight(havl.Latest)-power) + return power, true +} + +func (g *RolesVotingGroup) UpdateMembersHandler() dao_interfaces.MessageHandler { + return &updateMembersHandler{vg: g} +} + +func (g *RolesVotingGroup) MemberCount(height int64) uint32 { + val, ok := g.memberCount.Get("", height) + if !ok { + return 0 + } + + return val.(uint32) +} + +func (g *RolesVotingGroup) GetMembers(start, end string, limit uint64, height int64) []Member { + var members []Member + g.powerByAddr.Iterate(start, end, height, func(k string, v interface{}) bool { + if limit > 0 && uint64(len(members)) >= limit { + return true + } + + members = append(members, Member{ + Address: std.Address(k), + Power: v.(uint64), + }) + + return false + }) + return members +} + +func (v *RolesVotingGroup) Render(path string) string { + sb := strings.Builder{} + sb.WriteString("Member count: ") + sb.WriteString(strconv.FormatUint(uint64(v.MemberCount(havl.Latest)), 10)) + sb.WriteString("\n\n") + sb.WriteString("Total power: ") + sb.WriteString(strconv.FormatUint(v.TotalPowerAtHeight(havl.Latest), 10)) + sb.WriteString("\n\n") + sb.WriteString("Members:\n") + v.powerByAddr.Iterate("", "", havl.Latest, func(k string, v interface{}) bool { + sb.WriteString("- ") + sb.WriteString(k) + sb.WriteString(": ") + sb.WriteString(strconv.FormatUint(v.(uint64), 10)) + sb.WriteRune('\n') + return false + }) + + sb.WriteRune('\n') + return sb.String() +} diff --git a/gno/p/dao_roles_voting_group/roles_voting_group_test.gno b/gno/p/dao_roles_voting_group/roles_voting_group_test.gno new file mode 100644 index 0000000000..39eb14d08a --- /dev/null +++ b/gno/p/dao_roles_voting_group/roles_voting_group_test.gno @@ -0,0 +1,68 @@ +package dao_roles_voting_group + +import ( + "testing" + + "gno.land/p/demo/testutils" + dao_interfaces "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/dao_roles_group" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") +) + +func TestRolesVotingGroup(t *testing.T) { + rm := dao_roles_group.NewRolesGroup() + var j dao_interfaces.IRolesModule + j = rm + rv := NewRolesVotingGroup(j) + var i dao_interfaces.IVotingModule + i = rv + + { + got := i.TotalPowerAtHeight(0) + expected := uint64(0) + if got != expected { + t.Fatalf("expected %s, got %s.", expected, got) + } + } + + { + conf := rv.ConfigJSON() + expected := `{"totalPower":"0","members":"0"}` + if conf != expected { + t.Fatalf("expected %s, got %s.", expected, conf) + } + } + + rv.SetMemberPower(alice, 1) + + { + got := i.TotalPowerAtHeight(0) + expected := uint64(1) + if got != expected { + t.Fatalf("expected %s, got %s.", expected, got) + } + } + + j.NewRoleJSON("role1", `[ + {"resource": "resource1", "power": "2"} + ]`) + j.GrantRole(alice, "role1") + + { + got := i.VotingPowerAtHeight(alice, 0, []string{"resource1"}) + expected := uint64(2) + if got != expected { + t.Fatalf("expected %s, got %s.", expected, got) + } + + got = i.VotingPowerAtHeight(alice, 0, []string{"resource2"}) + expected = uint64(1) + if got != expected { + t.Fatalf("expected %s, got %s.", expected, got) + } + } +} diff --git a/gno/p/dao_voting_group/voting_group.gno b/gno/p/dao_voting_group/voting_group.gno index c76de71175..753d8a12c5 100644 --- a/gno/p/dao_voting_group/voting_group.gno +++ b/gno/p/dao_voting_group/voting_group.gno @@ -68,7 +68,8 @@ func (v *VotingGroup) GetMembersJSON(start, end string, limit uint64, height int return json.ArrayNode("", membersJSON).String() } -func (v *VotingGroup) VotingPowerAtHeight(addr std.Address, height int64) uint64 { +func (v *VotingGroup) VotingPowerAtHeight(addr std.Address, height int64, resources []string) uint64 { + _ = resources p, ok := v.powerByAddr.Get(addr.String(), height) if !ok { return 0 diff --git a/gno/p/role_manager/role_manager.gno b/gno/p/role_manager/role_manager.gno index 7a7b91a0d8..71a09226c0 100644 --- a/gno/p/role_manager/role_manager.gno +++ b/gno/p/role_manager/role_manager.gno @@ -163,7 +163,7 @@ func (rm *RoleManager) HasRole(user std.Address, roleName string) bool { return userRoles.Has(roleName) } -func (rm *RoleManager) IsRoleExist(roleName string) bool { +func (rm *RoleManager) RoleExists(roleName string) bool { return rm.roles.Has(roleName) } diff --git a/gno/r/dao_realm/dao_realm.gno b/gno/r/dao_realm/dao_realm.gno index b45ee65012..fc2d028149 100644 --- a/gno/r/dao_realm/dao_realm.gno +++ b/gno/r/dao_realm/dao_realm.gno @@ -1,5 +1,7 @@ package dao_realm +// TODO: Create two dao_realm example: Membership based & Roles based + import ( "std" "time" @@ -38,13 +40,6 @@ func init() { rolesModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IRolesModule { roles = dao_roles_group.NewRolesGroup() - roles.NewRole("admin") - roles.NewRole("moderator") - roles.NewRole("member") - roles.GrantRole("g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c", "admin") - roles.GrantRole("g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv", "moderator") - roles.GrantRole("g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a", "member") - roles.GrantRole("g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym", "member") return roles } diff --git a/gno/r/dao_roles_realm.gno/dao_roles_realm.gno b/gno/r/dao_roles_realm.gno/dao_roles_realm.gno new file mode 100644 index 0000000000..6319c3ff6b --- /dev/null +++ b/gno/r/dao_roles_realm.gno/dao_roles_realm.gno @@ -0,0 +1,157 @@ +package dao_roles_realm + +// TODO: Create two dao_realm example: Membership based & Roles based + +import ( + "std" + "time" + + dao_core "gno.land/p/teritori/dao_core" + dao_interfaces "gno.land/p/teritori/dao_interfaces" + proposal_single "gno.land/p/teritori/dao_proposal_single" + "gno.land/p/teritori/dao_roles_group" + roles_voting_group "gno.land/p/teritori/dao_roles_voting_group" + "gno.land/p/teritori/dao_utils" + "gno.land/r/demo/profile" + "gno.land/r/teritori/dao_registry" + "gno.land/r/teritori/social_feeds" + "gno.land/r/teritori/tori" +) + +// Example DAO realm + +var ( + daoCore dao_interfaces.IDAOCore + group *roles_voting_group.RolesVotingGroup + roles *dao_roles_group.RolesGroup + registered bool +) + +func init() { + rolesModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IRolesModule { + roles = dao_roles_group.NewRolesGroup() + roles.NewRoleJSON("admin", "[{\"resource\": \"social_feed\", \"power\": \"25\"}, {\"resource\": \"organizations\", \"power\": \"100\"}]") + roles.NewRoleJSON("moderator", "[{\"resource\": \"social_feed\", \"power\": \"10\"}]") + roles.NewRoleJSON("member", "[]") + roles.GrantRole("g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c", "admin") + roles.GrantRole("g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv", "moderator") + roles.GrantRole("g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a", "member") + roles.GrantRole("g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym", "member") + return roles + } + + votingModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IVotingModule { + group = roles_voting_group.NewRolesVotingGroup(core.RolesModule()) + group.SetMemberPower("g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c", 1) + group.SetMemberPower("g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv", 1) + group.SetMemberPower("g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a", 1) + group.SetMemberPower("g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym", 1) + group.SetMemberPower(std.GetOrigCaller(), 1) + return group + } + + // TODO: consider using factories that return multiple modules and handlers + + proposalModulesFactories := []dao_interfaces.ProposalModuleFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { + tt := proposal_single.PercentageThresholdPercent(100) + tq := proposal_single.PercentageThresholdPercent(100) + return proposal_single.NewDAOProposalSingle(core, &proposal_single.DAOProposalSingleOpts{ + MaxVotingPeriod: dao_utils.DurationTime(time.Hour * 24 * 42), + Threshold: &proposal_single.ThresholdThresholdQuorum{ + Threshold: &tt, // 1% + Quorum: &tq, // 1% + }, + }) + }, + } + + messageHandlersFactories := []dao_interfaces.MessageHandlerFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return group.UpdateMembersHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + // TODO: add a router to support multiple proposal modules + propMod := core.ProposalModules()[0] + return proposal_single.NewUpdateSettingsHandler(propMod.Module.(*proposal_single.DAOProposalSingle)) + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return tori.NewMintToriHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return tori.NewBurnToriHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return tori.NewChangeAdminHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return social_feeds.NewCreatePostHandler() + }, + } + + daoCore = dao_core.NewDAOCore(votingModuleFactory, rolesModuleFactory, proposalModulesFactories, messageHandlersFactories) + + // Register the DAO profile + profile.SetStringField(profile.DisplayName, "DAO Realm") + profile.SetStringField(profile.Bio, "Default testing DAO") + profile.SetStringField(profile.Avatar, "") + + // dao_registry.Register(func() dao_interfaces.IDAOCore { return daoCore }, "DAO Realm", "Default testing DAO", "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1080&fit=max") +} + +// FIXME: the registry is currently broken in 'gno test', see https://github.com/gnolang/gno/issues/1852 +// so we're exposing a way to register after DAO instantiation +func RegisterSelf() { + if registered { + panic("already registered") + } + + dao_registry.Register(func() dao_interfaces.IDAOCore { return daoCore }, "DAO Realm", "Default testing DAO", "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1080&fit=max") + registered = true +} + +func Render(path string) string { + return daoCore.Render(path) +} + +func VoteJSON(moduleIndex int, proposalID int, voteJSON string) { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.VoteJSON(proposalID, voteJSON) +} + +func Execute(moduleIndex int, proposalID int) { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.Execute(proposalID) +} + +func ProposeJSON(moduleIndex int, proposalJSON string) int { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + return module.Module.ProposeJSON(proposalJSON) +} + +func getProposalsJSON(moduleIndex int, limit int, startAfter string, reverse bool) string { + // move logic in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalsJSON(limit, startAfter, reverse) +} + +func getProposalJSON(moduleIndex int, proposalIndex int) string { + // move logic in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalJSON(proposalIndex) +} diff --git a/gno/r/dao_roles_realm.gno/dao_roles_realm_test.gno b/gno/r/dao_roles_realm.gno/dao_roles_realm_test.gno new file mode 100644 index 0000000000..aa817a27a9 --- /dev/null +++ b/gno/r/dao_roles_realm.gno/dao_roles_realm_test.gno @@ -0,0 +1,117 @@ +package dao_roles_realm + +import ( + "fmt" + "testing" + + "gno.land/p/demo/json" + "gno.land/p/teritori/dao_voting_group" + "gno.land/p/teritori/havl" +) + +func TestInit(t *testing.T) { + { + proposalsJSON := getProposalsJSON(0, 42, "TODO", false) + expected := `[]` + if proposalsJSON != expected { + t.Fatalf("Expected %s, got %s", expected, proposalsJSON) + } + } + + { + members := group.GetMembers("", "", 0, havl.Latest) + iSlice := make([]*json.Node, len(members)) + for i, v := range members { + iSlice[i] = v.ToJSON() + } + + membersJSON := json.ArrayNode("", iSlice).String() + expected := `[{"address":"g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv","power":"1"},{"address":"g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a","power":"1"},{"address":"g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c","power":"1"},{"address":"g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym","power":"1"},{"address":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","power":"1"}]` + if membersJSON != expected { + t.Fatalf("Expected:\n%s\nGot:\n%s", expected, membersJSON) + } + } +} + +func TestUpdateMembers(t *testing.T) { + var membersJSON string + + { + id := ProposeJSON(0, fmt.Sprintf(`{"title": "Test prop", "description": "A description", "messages": [{"type": "gno.land/p/teritori/dao_voting_group.UpdateMembers", "payload": [{"power": "2", "address": "g18syxa0vh0vmne90mwhtynjet0zgeqf6prh3ryy"}]}]}`)) + VoteJSON(0, id, `{"vote": "Yes", "rationale": "testing"}`) + Execute(0, id) + members := group.GetMembers("", "", 0, havl.Latest) + iSlice := make([]*json.Node, len(members)) + for i, v := range members { + iSlice[i] = v.ToJSON() + } + + membersJSON = json.ArrayNode("", iSlice).String() + expected := fmt.Sprintf(`[{"address":"g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv","power":"1"},{"address":"g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a","power":"1"},{"address":"g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c","power":"1"},{"address":"g18syxa0vh0vmne90mwhtynjet0zgeqf6prh3ryy","power":"2"},{"address":"g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym","power":"1"},{"address":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","power":"1"}]`) + if membersJSON != expected { + t.Errorf("Expected:\n%s\nGot:\n%s", expected, membersJSON) + } + + totalPower := group.TotalPowerAtHeight(havl.Latest) + if totalPower != 7 { + t.Errorf("Expected total power to be 6, got %d", totalPower) + } + } + + { + children := json.Must(json.Unmarshal([]byte(membersJSON))).MustArray() + if len(children) != 6 { + t.Errorf("Expected 6 members, got %d", len(children)) + } + + var member dao_voting_group.Member + member.FromJSON(children[0]) + + id := ProposeJSON(0, fmt.Sprintf(`{"title": "Test prop 2", "description": "A description", "messages": [{"type": "gno.land/p/teritori/dao_voting_group.UpdateMembers", "payload": [{"address": "%s", "power": "0"}]}]}`, member.Address.String())) + VoteJSON(0, id, `{"vote": "Yes", "rationale": "testing"}`) + Execute(0, id) + members := group.GetMembers("", "", 0, havl.Latest) + iSlice := make([]*json.Node, len(members)) + for i, v := range members { + iSlice[i] = v.ToJSON() + } + + membersJSON := json.ArrayNode("", iSlice).String() + expected := `[{"address":"g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a","power":"1"},{"address":"g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c","power":"1"},{"address":"g18syxa0vh0vmne90mwhtynjet0zgeqf6prh3ryy","power":"2"},{"address":"g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym","power":"1"},{"address":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","power":"1"}]` + if membersJSON != expected { + t.Errorf("Expected:\n%s\nGot:\n%s", expected, membersJSON) + } + + totalPower := group.TotalPowerAtHeight(havl.Latest) + if totalPower != 6 { + t.Errorf("Expected total power to be 6, got %d", totalPower) + } + } +} + +func TestUpdateSettings(t *testing.T) { + // not sure why but in this test the proposal ids start at 3 and the voting power is 5 when all tests are run, shouldn't tests be isolated? TODO: investigate + + { + id := ProposeJSON(0, fmt.Sprintf(`{"title": "Test prop", "description": "A description", "messages": [{"type": "gno.land/p/teritori/dao_proposal_single.UpdateSettings", "payload": {"threshold": {"thresholdQuorum": {"threshold": {"percent": 200}, "quorum": {"percent": 200}}}}}]}`)) + VoteJSON(0, id, `{"vote": "Yes", "rationale": "testing"}`) + Execute(0, id) + proposalJSON := getProposalJSON(0, id) + expected := fmt.Sprintf(`{"id":"%d","title":"Test prop","description":"A description","proposer":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","startHeight":"123","totalPower":"6","messages":[{"type":"gno.land/p/teritori/dao_proposal_single.UpdateSettings","payload":{"threshold":{"thresholdQuorum":{"threshold":{"percent":200},"quorum":{"percent":200}}}}}],"status":"Executed","votes":{"yes":"1","no":"0","abstain":"0"},"allowRevoting":false,"ballots":{"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm":{"power":"1","vote":"Yes","rationale":"testing"}},"expiration":{"atTime":"2009-03-27T23:31:30Z"},"threshold":{"thresholdQuorum":{"threshold":{"percent":100},"quorum":{"percent":100}}}}`, id) + if proposalJSON != expected { + t.Fatalf("Expected:\n%s\nGot:\n%s", expected, proposalJSON) + } + } + + { + // make sentiment proposal + id := ProposeJSON(0, fmt.Sprintf(`{"title": "Test prop", "description": "A description", "messages": []}`)) + VoteJSON(0, id, `{"vote": "Yes", "rationale": "testing"}`) + Execute(0, id) + proposalJSON := getProposalJSON(0, id) + expected := fmt.Sprintf(`{"id":"%d","title":"Test prop","description":"A description","proposer":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","startHeight":"123","totalPower":"6","messages":[],"status":"Executed","votes":{"yes":"1","no":"0","abstain":"0"},"allowRevoting":false,"ballots":{"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm":{"power":"1","vote":"Yes","rationale":"testing"}},"expiration":{"atTime":"2009-03-27T23:31:30Z"},"threshold":{"thresholdQuorum":{"threshold":{"percent":200},"quorum":{"percent":200}}}}`, id) + if proposalJSON != expected { + t.Fatalf("Expected:\n%s\nGot:\n%s", expected, proposalJSON) + } + } +} diff --git a/gno/r/dao_roles_realm.gno/gno.mod b/gno/r/dao_roles_realm.gno/gno.mod new file mode 100644 index 0000000000..8dbe10f69f --- /dev/null +++ b/gno/r/dao_roles_realm.gno/gno.mod @@ -0,0 +1 @@ +module gno.land/r/teritori/dao_roles_realm diff --git a/networks.json b/networks.json index 6f7eb497f6..f0a178c5a8 100644 --- a/networks.json +++ b/networks.json @@ -4522,6 +4522,7 @@ "modboardsPkgPath": "gno.land/r/teritori/modboards", "groupsPkgPath": "gno.land/r/teritori/groups", "votingGroupPkgPath": "gno.land/p/teritori/dao_voting_group", + "rolesVotingGroupPkgPath": "gno.land/p/teritori/dao_roles_voting_group", "rolesGroupPkgPath": "gno.land/p/teritori/dao_roles_group", "daoProposalSinglePkgPath": "gno.land/p/teritori/dao_proposal_single", "profilePkgPath": "gno.land/r/demo/profile", diff --git a/packages/screens/DAppStore/components/CheckboxDappStore.tsx b/packages/components/Checkbox.tsx similarity index 90% rename from packages/screens/DAppStore/components/CheckboxDappStore.tsx rename to packages/components/Checkbox.tsx index 7a83f31192..883883936f 100644 --- a/packages/screens/DAppStore/components/CheckboxDappStore.tsx +++ b/packages/components/Checkbox.tsx @@ -1,6 +1,6 @@ import { StyleProp, StyleSheet, View, ViewStyle } from "react-native"; -import checkSVG from "../../../../assets/icons/check.svg"; +import checkSVG from "../../assets/icons/check.svg"; import { SVG } from "@/components/SVG"; import { @@ -10,7 +10,7 @@ import { secondaryColor, } from "@/utils/style/colors"; -export const CheckboxDappStore: React.FC<{ +export const Checkbox: React.FC<{ isChecked?: boolean; style?: StyleProp; }> = ({ isChecked = false, style }) => { diff --git a/packages/hooks/dao/useDAOMember.ts b/packages/hooks/dao/useDAOMember.ts index 930034946d..57fceef3f6 100644 --- a/packages/hooks/dao/useDAOMember.ts +++ b/packages/hooks/dao/useDAOMember.ts @@ -52,7 +52,7 @@ const useDAOMember = ( const power = extractGnoNumber( await provider.evaluateExpression( packagePath, - `daoCore.VotingModule().VotingPowerAtHeight("${userAddress}", 0)`, + `daoCore.VotingModule().VotingPowerAtHeight("${userAddress}", 0, []string{})`, 0, ), ); diff --git a/packages/networks/gno-dev/index.ts b/packages/networks/gno-dev/index.ts index 1e8556e8b7..29032c6aa4 100644 --- a/packages/networks/gno-dev/index.ts +++ b/packages/networks/gno-dev/index.ts @@ -45,6 +45,7 @@ export const gnoDevNetwork: GnoNetworkInfo = { modboardsPkgPath: "gno.land/r/teritori/modboards", groupsPkgPath: "gno.land/r/teritori/groups", votingGroupPkgPath: "gno.land/p/teritori/dao_voting_group", + rolesVotingGroupPkgPath: "gno.land/p/teritori/dao_roles_voting_group", rolesGroupPkgPath: "gno.land/p/teritori/dao_roles_group", daoProposalSinglePkgPath: "gno.land/p/teritori/dao_proposal_single", profilePkgPath: "gno.land/r/demo/profile", diff --git a/packages/networks/types.ts b/packages/networks/types.ts index 3167b7dae4..35c4d63c0b 100644 --- a/packages/networks/types.ts +++ b/packages/networks/types.ts @@ -114,6 +114,7 @@ export type GnoNetworkInfo = NetworkInfoBase & { socialFeedsPkgPath?: string; socialFeedsDAOPkgPath?: string; votingGroupPkgPath?: string; + rolesVotingGroupPkgPath?: string; rolesGroupPkgPath?: string; daoProposalSinglePkgPath?: string; daoInterfacesPkgPath?: string; diff --git a/packages/screens/DAppStore/components/DAppBox.tsx b/packages/screens/DAppStore/components/DAppBox.tsx index f3537f1e23..dadb4d27f5 100644 --- a/packages/screens/DAppStore/components/DAppBox.tsx +++ b/packages/screens/DAppStore/components/DAppBox.tsx @@ -2,9 +2,8 @@ import React, { useEffect, useState } from "react"; import { Pressable, StyleProp, View } from "react-native"; import { useSelector } from "react-redux"; -import { CheckboxDappStore } from "./CheckboxDappStore"; - import { BrandText } from "@/components/BrandText"; +import { Checkbox } from "@/components/Checkbox"; import { SVGorImageIcon } from "@/components/SVG/SVGorImageIcon"; import { Box, BoxStyle } from "@/components/boxes/Box"; import { selectCheckedApps, setCheckedApp } from "@/store/slices/dapps-store"; @@ -95,7 +94,7 @@ export const DAppBox: React.FC<{ - {!alwaysOn && } + {!alwaysOn && } ); diff --git a/packages/screens/DAppStore/components/Dropdown.tsx b/packages/screens/DAppStore/components/Dropdown.tsx index 6207c6ebab..d6b46d5da1 100644 --- a/packages/screens/DAppStore/components/Dropdown.tsx +++ b/packages/screens/DAppStore/components/Dropdown.tsx @@ -2,11 +2,11 @@ import React, { useEffect, useState } from "react"; import { StyleProp, TouchableOpacity, View, ViewStyle } from "react-native"; import { useSelector } from "react-redux"; -import { CheckboxDappStore } from "./CheckboxDappStore"; import chevronDownSVG from "../../../../assets/icons/chevron-down.svg"; import chevronUpSVG from "../../../../assets/icons/chevron-up.svg"; import { BrandText } from "@/components/BrandText"; +import { Checkbox } from "@/components/Checkbox"; import { SVG } from "@/components/SVG"; import { SecondaryBox } from "@/components/boxes/SecondaryBox"; import { useDropdowns } from "@/hooks/useDropdowns"; @@ -47,7 +47,7 @@ const SelectableOption: React.FC<{ onPress={handleClick} style={[{ flexDirection: "row", alignItems: "center" }, style]} > - + {name} diff --git a/packages/screens/Message/components/CheckboxGroup.tsx b/packages/screens/Message/components/CheckboxGroup.tsx index 289d1fd1f0..efa1d5850b 100644 --- a/packages/screens/Message/components/CheckboxGroup.tsx +++ b/packages/screens/Message/components/CheckboxGroup.tsx @@ -3,38 +3,38 @@ import { TouchableOpacity, View } from "react-native"; import { Avatar } from "react-native-paper"; import FlexRow from "../../../components/FlexRow"; -import { CheckboxDappStore } from "../../DAppStore/components/CheckboxDappStore"; import { BrandText } from "@/components/BrandText"; +import { Checkbox } from "@/components/Checkbox"; import { SpacerColumn, SpacerRow } from "@/components/spacer"; import { neutral77, secondaryColor } from "@/utils/style/colors"; import { fontSemibold14 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; -export interface CheckboxItem { +export interface CheckboxMessageItem { id: string; name: string; avatar: string; checked: boolean; } -interface CheckboxGroupProps { - items: CheckboxItem[]; - onChange: (items: CheckboxItem[]) => void; +interface CheckboxMessageGroupProps { + items: CheckboxMessageItem[]; + onChange: (items: CheckboxMessageItem[]) => void; searchText: string; } -const Checkbox = ({ +const CheckboxMessage = ({ item, onPress, }: { - item: CheckboxItem; + item: CheckboxMessageItem; onPress: () => void; }) => { return ( <> - + @@ -49,12 +49,13 @@ const Checkbox = ({ ); }; -export const CheckboxGroup: React.FC = ({ +export const CheckboxGroup: React.FC = ({ items, onChange, searchText, }) => { - const [checkboxItems, setCheckboxItems] = useState(items); + const [checkboxItems, setCheckboxItems] = + useState(items); const handleCheckboxPress = (id: string) => { const newItems = checkboxItems; const itemIndex = newItems.findIndex((item) => item.id === id); @@ -86,7 +87,7 @@ export const CheckboxGroup: React.FC = ({ )} {!searchText.length && checkboxItems.map((item, index) => ( - handleCheckboxPress(item.id)} @@ -94,7 +95,7 @@ export const CheckboxGroup: React.FC = ({ ))} {!!searchText.length && searchItems.map((item, index) => ( - handleCheckboxPress(item.id)} diff --git a/packages/screens/Message/components/CreateGroup.tsx b/packages/screens/Message/components/CreateGroup.tsx index 021bf1252e..ad54ba7baa 100644 --- a/packages/screens/Message/components/CreateGroup.tsx +++ b/packages/screens/Message/components/CreateGroup.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react"; import { ScrollView, View } from "react-native"; import { useSelector } from "react-redux"; -import { CheckboxGroup, CheckboxItem } from "./CheckboxGroup"; +import { CheckboxGroup, CheckboxMessageItem } from "./CheckboxGroup"; import ModalBase from "../../../components/modals/ModalBase"; import { GroupInfo_Reply } from "@/api/weshnet/protocoltypes"; @@ -40,13 +40,13 @@ export const CreateGroup = ({ onClose }: CreateGroupProps) => { const [searchText, setSearchText] = useState(""); const conversations = useSelector(selectConversationList); - const handleChange = (items: CheckboxItem[]) => { + const handleChange = (items: CheckboxMessageItem[]) => { setCheckedContacts( items.filter((item) => !item.checked).map((item) => item.id), ); }; - const items: CheckboxItem[] = useMemo(() => { + const items: CheckboxMessageItem[] = useMemo(() => { return conversations .filter((conv) => conv.type === "contact") .map((item) => { diff --git a/packages/screens/Organizations/components/MembershipOrg/MembershipDeployerSteps.tsx b/packages/screens/Organizations/components/MembershipOrg/MembershipDeployerSteps.tsx index 92ae29cedd..1a4b680cef 100644 --- a/packages/screens/Organizations/components/MembershipOrg/MembershipDeployerSteps.tsx +++ b/packages/screens/Organizations/components/MembershipOrg/MembershipDeployerSteps.tsx @@ -60,6 +60,7 @@ export const MembershipDeployerSteps: React.FC<{ const pkgPath = await adenaDeployGnoDAO( network.id, selectedWallet?.address!, + organizationData?.structure!, { name, maxVotingPeriodSeconds: diff --git a/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx b/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx index 20ad91e8f3..bd6b04d754 100644 --- a/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx +++ b/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx @@ -62,7 +62,11 @@ export const RolesDeployerSteps: React.FC<{ case NetworkKind.Gno: { const name = organizationData?.associatedHandle!; const roles = - rolesSettingsFormData?.roles?.map((role) => role.name.trim()) || []; + rolesSettingsFormData?.roles?.map((role) => ({ + name: role.name.trim(), + color: role.color, + resources: role.resources, + })) || []; const initialMembers = (memberSettingsFormData?.members || []).map( (member) => ({ address: member.addr, @@ -75,6 +79,7 @@ export const RolesDeployerSteps: React.FC<{ const pkgPath = await adenaDeployGnoDAO( network.id, selectedWallet?.address!, + organizationData?.structure!, { name, maxVotingPeriodSeconds: diff --git a/packages/screens/Organizations/components/RolesOrg/RolesModalCreateRole.tsx b/packages/screens/Organizations/components/RolesOrg/RolesModalCreateRole.tsx new file mode 100644 index 0000000000..1e5a57cfdf --- /dev/null +++ b/packages/screens/Organizations/components/RolesOrg/RolesModalCreateRole.tsx @@ -0,0 +1,98 @@ +import { Control } from "react-hook-form"; +import { ScrollView, TouchableOpacity, View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { Checkbox } from "@/components/Checkbox"; +import { Box } from "@/components/boxes/Box"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { Label, TextInputCustom } from "@/components/inputs/TextInputCustom"; +import ModalBase from "@/components/modals/ModalBase"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { neutral33 } from "@/utils/style/colors"; +import { fontSemibold18 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { RolesSettingFormType } from "@/utils/types/organizations"; + +interface RolesModalCreateRoleProps { + modalVisible: boolean; + rolesIndexes: number[]; + resources: { name: string; resources: string[]; value: boolean }[]; + control: Control; + onCloseModal: () => void; + onCheckboxChange: (index: number) => void; + addRoleField: () => void; +} + +export const RolesModalCreateRole: React.FC = ({ + modalVisible, + rolesIndexes, + resources, + control, + onCloseModal, + onCheckboxChange, + addRoleField, +}) => { + return ( + + + + control={control} + noBrokenCorners + name={`roles.${rolesIndexes.length}.name`} + label="Role name" + placeholder="Role name" + rules={{ required: true }} + placeHolder="Role name" + /> + + + control={control} + noBrokenCorners + name={`roles.${rolesIndexes.length}.color`} + label="Role color" + placeholder="Role color" + placeHolder="Role color" + /> + + + + + + + {/* TODO: Refactor Checkbox to make it a global component instead of Dapp!*/} + {resources.map((resource, index) => ( + + onCheckboxChange(index)}> + + + + {resource.name} + + + ))} + + + + + + + + + + ); +}; diff --git a/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx b/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx index cdaccfc245..00a4a26e11 100644 --- a/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx +++ b/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx @@ -169,7 +169,9 @@ export const RolesReviewInformationSection: React.FC< ( - {role.name} + + {role.name} features: {role.resources?.join(", ")} + )} /> {rolesSettingData?.roles.length !== index + 1 && ( diff --git a/packages/screens/Organizations/components/RolesOrg/RolesSettingsSection.tsx b/packages/screens/Organizations/components/RolesOrg/RolesSettingsSection.tsx index 74bdbeb867..5ae90bd494 100644 --- a/packages/screens/Organizations/components/RolesOrg/RolesSettingsSection.tsx +++ b/packages/screens/Organizations/components/RolesOrg/RolesSettingsSection.tsx @@ -1,19 +1,25 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { Pressable, View } from "react-native"; -import { ScrollView } from "react-native-gesture-handler"; +import { FlatList, ScrollView } from "react-native-gesture-handler"; +import { RolesModalCreateRole } from "./RolesModalCreateRole"; import trashSVG from "../../../../../assets/icons/trash.svg"; import { BrandText } from "@/components/BrandText"; import { SVG } from "@/components/SVG"; import { PrimaryButton } from "@/components/buttons/PrimaryButton"; import { SecondaryButton } from "@/components/buttons/SecondaryButton"; -import { TextInputCustom } from "@/components/inputs/TextInputCustom"; -import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { SpacerColumn } from "@/components/spacer"; +import { TableCell } from "@/components/table/TableCell"; +import { TableHeader } from "@/components/table/TableHeader"; +import { TableRow } from "@/components/table/TableRow"; +import { TableTextCell } from "@/components/table/TableTextCell"; +import { TableWrapper } from "@/components/table/TableWrapper"; +import { TableColumns } from "@/components/table/utils"; import { neutral33 } from "@/utils/style/colors"; import { fontSemibold28 } from "@/utils/style/fonts"; -import { layout } from "@/utils/style/layout"; +import { layout, screenContentMaxWidthLarge } from "@/utils/style/layout"; import { ROLES_BASED_ORGANIZATION_STEPS, RolesSettingFormType, @@ -26,25 +32,76 @@ interface RolesSettingsSectionProps { export const RolesSettingsSection: React.FC = ({ onSubmit, }) => { - const { handleSubmit, control, unregister } = useForm(); - + const { + handleSubmit, + control, + unregister, + register, + setValue, + resetField, + getValues, + } = useForm(); + const [modalVisible, setModalVisible] = useState(false); const [rolesIndexes, setRolesIndexes] = useState([]); + const [resources, setResources] = + useState<{ name: string; resources: string[]; value: boolean }[]>( + fakeResources, + ); const removeRoleField = (id: number, index: number) => { unregister(`roles.${index}.name`); unregister(`roles.${index}.color`); + unregister(`roles.${index}.resources`); if (rolesIndexes.length > 0) { const copyIndex = [...rolesIndexes].filter((i) => i !== id); setRolesIndexes(copyIndex); } }; + const resetModal = () => { + resetField(`roles.${rolesIndexes.length}.name`); + resetField(`roles.${rolesIndexes.length}.color`); + resetField(`roles.${rolesIndexes.length}.resources`); + }; + + const onOpenModal = () => { + resetModal(); + setResources(fakeResources.map((r) => ({ ...r, value: false }))); + setModalVisible(true); + }; + const addRoleField = () => { + register(`roles.${rolesIndexes.length}.resources`); + const selectedResources = resources + .filter((r) => r.value) + .flatMap((r) => r.resources); + setValue(`roles.${rolesIndexes.length}.resources`, selectedResources); + console.log(`Selected resources: ${selectedResources}`); setRolesIndexes([...rolesIndexes, Math.floor(Math.random() * 200000)]); + setModalVisible(false); + }; + + const onCloseModal = () => { + setModalVisible(false); + }; + + const onCheckboxChange = (index: number) => { + const copyResources = [...resources]; + copyResources[index].value = !copyResources[index].value; + setResources(copyResources); }; return ( + = ({ Roles - {rolesIndexes.map((id, index) => ( - - - - control={control} - noBrokenCorners - name={`roles.${index}.name`} - label="Role name" - placeholder="Role name" - rules={{ required: true }} - placeHolder="Role name" - /> - - - - - control={control} - noBrokenCorners - name={`roles.${index}.color`} - label="Role color" - placeholder="Role color" - placeHolder="Role color" - /> - - - - { - removeRoleField(id, index); - }} - > - - - - - ))} - + + Roles table + + + + { + const role = getValues(`roles.${index}`); + + if (!role) { + return null; + } + + return ( + + + + + ); + }} + keyExtractor={(item) => item.toString()} + /> + + + + = ({ ); }; + +const RoleTableRow: React.FC<{ + role: { name: string; color: string; resources: string[] | undefined }; + removeRoleField: (id: number, index: number) => void; + id: number; + index: number; +}> = ({ role, removeRoleField, id, index }) => { + return ( + + + {role.name} + + + {role.color} + + + {role.resources?.join(", ") || "No resources defined"} + + + + { + removeRoleField(id, index); + }} + > + + + + + + ); +}; + +const columns: TableColumns = { + name: { + label: "Name", + flex: 1, + minWidth: 120, + }, + color: { + label: "Color", + flex: 1, + minWidth: 60, + }, + resources: { + label: "Resources", + flex: 1.5, + minWidth: 150, + }, + delete: { + label: "Delete", + flex: 0.25, + minWidth: 30, + }, +}; + +// TODO: Create a hook to get all the resources +const fakeResources = [ + { + name: "Organizations", + resources: [], + value: false, + }, + { + name: "Social Feed", + resources: ["gno.land/r/teritori/social_feeds.CreatePost"], + value: false, + }, + { + name: "Marketplace", + resources: [], + value: false, + }, + { + name: "Launchpad NFT", + resources: [], + value: false, + }, + { + name: "Launchpad ERC20", + resources: [], + value: false, + }, + { + name: "Name Service", + resources: [], + value: false, + }, + { + name: "Multisig Wallet", + resources: [], + value: false, + }, + { + name: "Projects", + resources: [], + value: false, + }, +]; diff --git a/packages/screens/Organizations/components/TokenOrg/TokenDeployerSteps.tsx b/packages/screens/Organizations/components/TokenOrg/TokenDeployerSteps.tsx index 873cc77500..2d16fcd59f 100644 --- a/packages/screens/Organizations/components/TokenOrg/TokenDeployerSteps.tsx +++ b/packages/screens/Organizations/components/TokenOrg/TokenDeployerSteps.tsx @@ -23,6 +23,7 @@ import { getDuration, getPercent } from "@/utils/gnodao/helpers"; import { ConfigureVotingFormType, CreateDaoFormType, + DaoType, LAUNCHING_PROCESS_STEPS, TOKEN_ORGANIZATION_DEPLOYER_STEPS, TokenSettingFormType, @@ -61,6 +62,7 @@ export const TokenDeployerSteps: React.FC<{ const pkgPath = await adenaDeployGnoDAO( network.id, selectedWallet?.address!, + DaoType.TOKEN_BASED, { name, maxVotingPeriodSeconds: diff --git a/packages/utils/gnodao/deploy.ts b/packages/utils/gnodao/deploy.ts index 59c255e911..347c45bc24 100644 --- a/packages/utils/gnodao/deploy.ts +++ b/packages/utils/gnodao/deploy.ts @@ -1,5 +1,7 @@ -import { mustGetGnoNetwork } from "../../networks"; +import { generateMembershipDAOSource } from "./generateMembershipDAOSource"; +import { generateRolesDAOSource } from "./generateRolesDAOSource"; import { adenaAddPkg } from "../gno"; +import { DaoType } from "../types/organizations"; interface GnoDAOMember { address: string; @@ -7,10 +9,16 @@ interface GnoDAOMember { roles: string[]; } -interface GnoDAOConfig { +interface GnoDAORole { + name: string; + color: string; + resources: string[] | undefined; +} + +export interface GnoDAOConfig { name: string; maxVotingPeriodSeconds: number; - roles: string[] | undefined; + roles: GnoDAORole[] | undefined; initialMembers: GnoDAOMember[]; thresholdPercent: number; quorumPercent: number; @@ -19,149 +27,18 @@ interface GnoDAOConfig { imageURI: string; } -const generateDAORealmSource = (networkId: string, conf: GnoDAOConfig) => { - const network = mustGetGnoNetwork(networkId); - return `package ${conf.name} - - import ( - "time" - - dao_core "${network.daoCorePkgPath}" - dao_interfaces "${network.daoInterfacesPkgPath}" - proposal_single "${network.daoProposalSinglePkgPath}" - "${network.rolesGroupPkgPath}" - "${network.daoUtilsPkgPath}" - "${network.profilePkgPath}" - voting_group "${network.votingGroupPkgPath}" - "${network.daoRegistryPkgPath}" - "${network.socialFeedsPkgPath}" - ) - -var ( - daoCore dao_interfaces.IDAOCore - group *voting_group.VotingGroup - roles *dao_roles_group.RolesGroup - registered bool -) - -func init() { - votingModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IVotingModule { - group = voting_group.NewVotingGroup() - ${conf.initialMembers - .map( - (member) => - `group.SetMemberPower("${member.address}", ${member.weight})`, - ) - .join("\n\t")} - return group - } - - rolesModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IRolesModule { - roles = dao_roles_group.NewRolesGroup() - ${(conf.roles ?? []).map((role) => `roles.NewRole("${role}");`).join("\n\t")} - ${conf.initialMembers.map((member) => member.roles.map((role) => `roles.GrantRole("${member.address}", "${role}")`).join("\n\t"))} - return roles - } - - - // TODO: consider using factories that return multiple modules and handlers - - proposalModulesFactories := []dao_interfaces.ProposalModuleFactory{ - func(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { - tt := proposal_single.PercentageThresholdPercent(${Math.ceil( - conf.thresholdPercent * 100, - )}) // ${Math.ceil(conf.thresholdPercent * 100) / 100}% - tq := proposal_single.PercentageThresholdPercent(${Math.ceil( - conf.quorumPercent * 100, - )}) // ${Math.ceil(conf.quorumPercent * 100) / 100}% - return proposal_single.NewDAOProposalSingle(core, &proposal_single.DAOProposalSingleOpts{ - MaxVotingPeriod: dao_utils.DurationTime(time.Second * ${conf.maxVotingPeriodSeconds}), - Threshold: &proposal_single.ThresholdThresholdQuorum{ - Threshold: &tt, - Quorum: &tq, - }, - }) - }, - } - - messageHandlersFactories := []dao_interfaces.MessageHandlerFactory{ - func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { - return group.UpdateMembersHandler() - }, - func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { - // TODO: add a router to support multiple proposal modules - propMod := core.ProposalModules()[0] - return proposal_single.NewUpdateSettingsHandler(propMod.Module.(*proposal_single.DAOProposalSingle)) - }, - func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { - return social_feeds.NewCreatePostHandler() - }, - } - - daoCore = dao_core.NewDAOCore(votingModuleFactory, rolesModuleFactory, proposalModulesFactories, messageHandlersFactories) - - // Register the DAO profile - profile.SetStringField(profile.DisplayName, "${conf.displayName}") - profile.SetStringField(profile.Bio, "${conf.description}") - profile.SetStringField(profile.Avatar, "${conf.imageURI}") - - dao_registry.Register(func() dao_interfaces.IDAOCore { return daoCore }, "${conf.displayName}", "${conf.description}", "${conf.imageURI}") - } - - func Render(path string) string { - return daoCore.Render(path) - } - - func VoteJSON(moduleIndex int, proposalID int, voteJSON string) { - // move check in dao core - module := dao_core.GetProposalModule(daoCore, moduleIndex) - if !module.Enabled { - panic("proposal module is not enabled") - } - - module.Module.VoteJSON(proposalID, voteJSON) - } - - func Execute(moduleIndex int, proposalID int) { - // move check in dao core - module := dao_core.GetProposalModule(daoCore, moduleIndex) - if !module.Enabled { - panic("proposal module is not enabled") - } - - module.Module.Execute(proposalID) - } - - func ProposeJSON(moduleIndex int, proposalJSON string) int { - // move check in dao core - module := dao_core.GetProposalModule(daoCore, moduleIndex) - if !module.Enabled { - panic("proposal module is not enabled") - } - - return module.Module.ProposeJSON(proposalJSON) - } - - func getProposalsJSON(moduleIndex int, limit int, startAfter string, reverse bool) string { - // move logic in dao core - module := dao_core.GetProposalModule(daoCore, moduleIndex) - return module.Module.ProposalsJSON(limit, startAfter, reverse) - } - - func getProposalJSON(moduleIndex int, proposalIndex int) string { - // move logic in dao core - module := dao_core.GetProposalModule(daoCore, moduleIndex) - return module.Module.ProposalJSON(proposalIndex) - } -`; -}; - export const adenaDeployGnoDAO = async ( networkId: string, creator: string, + structure: DaoType, conf: GnoDAOConfig, ) => { - const source = generateDAORealmSource(networkId, conf); + let source = ""; + if (structure === DaoType.MEMBER_BASED) { + source = generateMembershipDAOSource(networkId, conf); + } else { + source = generateRolesDAOSource(networkId, conf); + } const pkgPath = `gno.land/r/${creator}/${conf.name}`; await adenaAddPkg( networkId, diff --git a/packages/utils/gnodao/generateMembershipDAOSource.ts b/packages/utils/gnodao/generateMembershipDAOSource.ts new file mode 100644 index 0000000000..8e0fe7542c --- /dev/null +++ b/packages/utils/gnodao/generateMembershipDAOSource.ts @@ -0,0 +1,143 @@ +import { GnoDAOConfig } from "./deploy"; +import { mustGetGnoNetwork } from "../../networks"; + +// TODO: Allow the role modules to be optional and don't use in MembershipDAO +export const generateMembershipDAOSource = ( + networkId: string, + conf: GnoDAOConfig, +) => { + const network = mustGetGnoNetwork(networkId); + return `package ${conf.name} + + import ( + "time" + + dao_core "${network.daoCorePkgPath}" + dao_interfaces "${network.daoInterfacesPkgPath}" + proposal_single "${network.daoProposalSinglePkgPath}" + "${network.rolesGroupPkgPath}" + "${network.daoUtilsPkgPath}" + "${network.profilePkgPath}" + voting_group "${network.votingGroupPkgPath}" + "${network.daoRegistryPkgPath}" + "${network.socialFeedsPkgPath}" + ) + +var ( + daoCore dao_interfaces.IDAOCore + group *voting_group.VotingGroup + roles *dao_roles_group.RolesGroup + registered bool +) + +func init() { + votingModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IVotingModule { + group = voting_group.NewVotingGroup() + ${conf.initialMembers + .map( + (member) => + `group.SetMemberPower("${member.address}", ${member.weight})`, + ) + .join("\n\t")} + return group + } + + rolesModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IRolesModule { + roles = dao_roles_group.NewRolesGroup() + ${(conf.roles ?? []).map((role) => `roles.NewRole("${role}", "");`).join("\n\t")} + ${conf.initialMembers.map((member) => member.roles.map((role) => `roles.GrantRole("${member.address}", "${role}")`).join("\n\t"))} + return roles + } + + + // TODO: consider using factories that return multiple modules and handlers + + proposalModulesFactories := []dao_interfaces.ProposalModuleFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { + tt := proposal_single.PercentageThresholdPercent(${Math.ceil( + conf.thresholdPercent * 100, + )}) // ${Math.ceil(conf.thresholdPercent * 100) / 100}% + tq := proposal_single.PercentageThresholdPercent(${Math.ceil( + conf.quorumPercent * 100, + )}) // ${Math.ceil(conf.quorumPercent * 100) / 100}% + return proposal_single.NewDAOProposalSingle(core, &proposal_single.DAOProposalSingleOpts{ + MaxVotingPeriod: dao_utils.DurationTime(time.Second * ${conf.maxVotingPeriodSeconds}), + Threshold: &proposal_single.ThresholdThresholdQuorum{ + Threshold: &tt, + Quorum: &tq, + }, + }) + }, + } + + messageHandlersFactories := []dao_interfaces.MessageHandlerFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return group.UpdateMembersHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + // TODO: add a router to support multiple proposal modules + propMod := core.ProposalModules()[0] + return proposal_single.NewUpdateSettingsHandler(propMod.Module.(*proposal_single.DAOProposalSingle)) + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return social_feeds.NewCreatePostHandler() + }, + } + + daoCore = dao_core.NewDAOCore(votingModuleFactory, rolesModuleFactory, proposalModulesFactories, messageHandlersFactories) + + // Register the DAO profile + profile.SetStringField(profile.DisplayName, "${conf.displayName}") + profile.SetStringField(profile.Bio, "${conf.description}") + profile.SetStringField(profile.Avatar, "${conf.imageURI}") + + dao_registry.Register(func() dao_interfaces.IDAOCore { return daoCore }, "${conf.displayName}", "${conf.description}", "${conf.imageURI}") + } + + func Render(path string) string { + return daoCore.Render(path) + } + + func VoteJSON(moduleIndex int, proposalID int, voteJSON string) { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.VoteJSON(proposalID, voteJSON) + } + + func Execute(moduleIndex int, proposalID int) { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.Execute(proposalID) + } + + func ProposeJSON(moduleIndex int, proposalJSON string) int { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + return module.Module.ProposeJSON(proposalJSON) + } + + func getProposalsJSON(moduleIndex int, limit int, startAfter string, reverse bool) string { + // move logic in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalsJSON(limit, startAfter, reverse) + } + + func getProposalJSON(moduleIndex int, proposalIndex int) string { + // move logic in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalJSON(proposalIndex) + } +`; +}; diff --git a/packages/utils/gnodao/generateRolesDAOSource.ts b/packages/utils/gnodao/generateRolesDAOSource.ts new file mode 100644 index 0000000000..93d30f392a --- /dev/null +++ b/packages/utils/gnodao/generateRolesDAOSource.ts @@ -0,0 +1,159 @@ +import { GnoDAOConfig } from "./deploy"; +import { mustGetGnoNetwork } from "../../networks"; + +export const generateRolesDAOSource = ( + networkId: string, + conf: GnoDAOConfig, +) => { + const network = mustGetGnoNetwork(networkId); + return `package ${conf.name} + + import ( + "time" + + dao_core "${network.daoCorePkgPath}" + dao_interfaces "${network.daoInterfacesPkgPath}" + proposal_single "${network.daoProposalSinglePkgPath}" + "${network.rolesGroupPkgPath}" + "${network.daoUtilsPkgPath}" + "${network.profilePkgPath}" + voting_group "${network.rolesVotingGroupPkgPath}" + "${network.daoRegistryPkgPath}" + "${network.socialFeedsPkgPath}" + ) + +var ( + daoCore dao_interfaces.IDAOCore + group *voting_group.RolesVotingGroup + roles *dao_roles_group.RolesGroup + registered bool +) + +func init() { + rolesModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IRolesModule { + roles = dao_roles_group.NewRolesGroup() + ${(conf.roles ?? []) + .map( + (role) => + `roles.NewRoleJSON("${role.name}", "[${(role.resources ?? []) + .map( + (resource) => + `{\\"resource\\": \\"${resource}\\", \\"power\\": \\"999\\"}`, + ) + .join(", ")}]")`, + ) + .join("\n\t")} + ${conf.initialMembers + .filter((member) => member.roles.length > 0) + .map((member) => + member.roles + .map((role) => `roles.GrantRole("${member.address}", "${role}")`) + .join("\n\t"), + ) + .join("\n\t")} + return roles + } + + votingModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IVotingModule { + group = voting_group.NewRolesVotingGroup(core.RolesModule()) + ${conf.initialMembers + .map( + (member) => + `group.SetMemberPower("${member.address}", ${member.weight})`, + ) + .join("\n\t")} + return group + } + + + // TODO: consider using factories that return multiple modules and handlers + + proposalModulesFactories := []dao_interfaces.ProposalModuleFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { + tt := proposal_single.PercentageThresholdPercent(${Math.ceil( + conf.thresholdPercent * 100, + )}) // ${Math.ceil(conf.thresholdPercent * 100) / 100}% + tq := proposal_single.PercentageThresholdPercent(${Math.ceil( + conf.quorumPercent * 100, + )}) // ${Math.ceil(conf.quorumPercent * 100) / 100}% + return proposal_single.NewDAOProposalSingle(core, &proposal_single.DAOProposalSingleOpts{ + MaxVotingPeriod: dao_utils.DurationTime(time.Second * ${conf.maxVotingPeriodSeconds}), + Threshold: &proposal_single.ThresholdThresholdQuorum{ + Threshold: &tt, + Quorum: &tq, + }, + }) + }, + } + + messageHandlersFactories := []dao_interfaces.MessageHandlerFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return group.UpdateMembersHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + // TODO: add a router to support multiple proposal modules + propMod := core.ProposalModules()[0] + return proposal_single.NewUpdateSettingsHandler(propMod.Module.(*proposal_single.DAOProposalSingle)) + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return social_feeds.NewCreatePostHandler() + }, + } + + daoCore = dao_core.NewDAOCore(votingModuleFactory, rolesModuleFactory, proposalModulesFactories, messageHandlersFactories) + + // Register the DAO profile + profile.SetStringField(profile.DisplayName, "${conf.displayName}") + profile.SetStringField(profile.Bio, "${conf.description}") + profile.SetStringField(profile.Avatar, "${conf.imageURI}") + + dao_registry.Register(func() dao_interfaces.IDAOCore { return daoCore }, "${conf.displayName}", "${conf.description}", "${conf.imageURI}") + } + + func Render(path string) string { + return daoCore.Render(path) + } + + func VoteJSON(moduleIndex int, proposalID int, voteJSON string) { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.VoteJSON(proposalID, voteJSON) + } + + func Execute(moduleIndex int, proposalID int) { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.Execute(proposalID) + } + + func ProposeJSON(moduleIndex int, proposalJSON string) int { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + return module.Module.ProposeJSON(proposalJSON) + } + + func getProposalsJSON(moduleIndex int, limit int, startAfter string, reverse bool) string { + // move logic in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalsJSON(limit, startAfter, reverse) + } + + func getProposalJSON(moduleIndex int, proposalIndex int) string { + // move logic in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalJSON(proposalIndex) + } +`; +}; diff --git a/packages/utils/types/organizations.ts b/packages/utils/types/organizations.ts index 66673bdeaa..eb1cf9457c 100644 --- a/packages/utils/types/organizations.ts +++ b/packages/utils/types/organizations.ts @@ -42,7 +42,7 @@ export type MembershipMemberSettingFormType = { // ROLES BASED ORGANIZATION FORM TYPES export type RolesSettingFormType = { - roles: { name: string; color: string }[]; + roles: { name: string; color: string; resources: string[] | undefined }[]; }; export type RolesMemberSettingFormType = {