Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

utils.gno

6.19 Kb · 206 lines
  1package basedao
  2
  3import (
  4	"strings"
  5	"time"
  6
  7	"gno.land/p/demo/svg"
  8	"gno.land/p/moul/md"
  9	ntavl "gno.land/p/nt/avl/v0" // svg.Canvas.Style is a *nt/avl/v0.Tree — keep that ABI at the svg boundary
 10	"gno.land/p/nt/seqid/v0"
 11	"gno.land/p/nt/ufmt/v0"
 12	"gno.land/p/samcrew/avl"
 13	"gno.land/p/samcrew/avl/pager"
 14	"gno.land/p/samcrew/daokit"
 15)
 16
 17// Truncates a string to a maximum length, adding "..." in the middle if it exceeds the limit.
 18func TruncateMiddle(s string, max ...int) string {
 19	defMax := 12
 20	if len(max) > 0 {
 21		defMax = max[0]
 22	}
 23	runes := []rune(s)
 24	if len(runes) <= defMax {
 25		return s
 26	}
 27	if defMax <= 3 {
 28		return string(runes[:defMax])
 29	}
 30	half := (defMax - 3) / 2
 31	return ufmt.Sprintf("%s...%s", string(runes[:half]), string(runes[len(runes)-half:]))
 32}
 33
 34// Formats an address as a clickable link in Markdown.
 35func RenderAddressLink(addr string) string {
 36	return md.Link(TruncateMiddle(addr), ufmt.Sprintf("/u/%s", addr))
 37}
 38
 39// Replaces all occurrences of roles in the given string with clickable links in Markdown.
 40func (d *DAOPrivate) RenderWithRolesLinks(s string) string {
 41	roles := d.Members.GetRoles()
 42	for _, role := range roles {
 43		// AddRole refuses an empty name, but Roles is an exported avl.Tree that
 44		// a same-realm write or a MigrateFn can seed directly. An empty needle
 45		// makes ReplaceAll match between every character, so skipping here keeps
 46		// the render path safe however the store was filled.
 47		if role == "" {
 48			continue
 49		}
 50		s = strings.ReplaceAll(s, role, d.RenderRoleLink(role))
 51	}
 52	return s
 53}
 54
 55// Formats a role as a clickable link in Markdown.
 56func (d *DAOPrivate) RenderRoleLink(role string) string {
 57	pkgPath := d.Realm.PkgPath()
 58	linkPath := getLinkPath(pkgPath)
 59	roleName := d.Members.RoleInfo(role).Name
 60
 61	if role == "" {
 62		return "*No role assigned*"
 63	}
 64	return ufmt.Sprintf("[%s](%s:%s/%s)", roleName, linkPath, "role", role)
 65}
 66
 67// Formats a role as a clickable link in Markdown, with colored chip at left.
 68func (d *DAOPrivate) RenderRoleLinkWithChip(role string) string {
 69	pkgPath := d.Realm.PkgPath()
 70	linkPath := getLinkPath(pkgPath)
 71
 72	if role == "" {
 73		return ufmt.Sprintf("%s %s", d.RoleColoredChip(""), "*No role assigned*")
 74	}
 75	return ufmt.Sprintf("[%s %s](%s:%s/%s)", d.RoleColoredChip(role), role, linkPath, "role", role)
 76}
 77
 78// Formats a time.Time object as a string with UTC offset.
 79func FormatDateTimeWithUTCOffset(t time.Time) string {
 80	_, offsetSeconds := t.Zone()
 81	hours := offsetSeconds / 3600
 82	minutes := (offsetSeconds % 3600) / 60
 83	sign := "+"
 84	if hours < 0 || minutes < 0 {
 85		sign = "-"
 86		hours = -hours
 87		minutes = -minutes
 88	}
 89	return ufmt.Sprintf("%s UTC%s%02d:%02d",
 90		t.Format("2006-01-02 15:04:05"),
 91		sign, hours, minutes)
 92}
 93
 94// Returns a colored chip svg for the given role. If no role is given, a gray chip is returned.
 95// It can optionally take a custom svg.Canvas to define the size and style of the chip.
 96func (d *DAOPrivate) RoleColoredChip(role string, canvas ...*svg.Canvas) string {
 97	roleName := ""
 98	chipColor := ""
 99	canvasWidth := 16
100	canvasHeight := 16
101	canvasElem := []svg.Elem{}
102	canvasStyle := ntavl.NewTree()
103
104	if canvas != nil && len(canvas) > 0 {
105		canvasWidth = canvas[0].Width
106		canvasHeight = canvas[0].Height
107		canvasElem = canvas[0].Elems
108		canvasStyle = canvas[0].Style
109	}
110
111	if role == "" {
112		chipColor = "#CDCDCD"
113	} else {
114		info := d.Members.RoleInfo(role)
115		chipColor = info.Color
116		roleName = info.Name
117	}
118	chipCanvas := &svg.Canvas{
119		Width:  canvasWidth,
120		Height: canvasHeight,
121		Elems:  canvasElem,
122		Style:  canvasStyle,
123	}
124
125	chipCanvas.Append(svg.NewRectangle(0, 0, canvasWidth, canvasHeight, chipColor))
126	return chipCanvas.Render(roleName + " colored chip")
127}
128
129// Renders a table of members with their display name, address, roles, and profile link.
130// It supports pagination.
131func (d *DAOPrivate) RenderMembersTable(path string, members *avl.Tree) string {
132	pkgPath := d.Realm.PkgPath()
133	linkPath := getLinkPath(pkgPath)
134	const pageSize = 10
135	pager := pager.NewPager(members, pageSize, false)
136	page := pager.MustGetPageByPath(path)
137
138	s := ""
139	s += ufmt.Sprintf("| **Name** | **Address 🔗** | **Roles 🎭** | **Profile** |\n")
140	s += ufmt.Sprintf("|----------|----------------|--------------|-------------|\n")
141
142	for _, item := range page.Items {
143		displayName := d.GetProfileString(address(item.Key), "DisplayName", FALLBACK_DISPLAY_NAME)
144		roles := d.Members.GetMemberRoles(item.Key)
145		rolesStr := strings.Join(roles, ", ")
146
147		if len(roles) == 0 {
148			rolesStr = d.RenderRoleLinkWithChip("")
149		}
150		for _, role := range roles {
151			// Same empty-needle hazard as RenderWithRolesLinks: an empty role
152			// name matches between every character. AddRole refuses one, but
153			// Roles is an exported avl.Tree that can be seeded around it — and
154			// once it holds "", AddRoleToMember will happily assign it.
155			if role == "" {
156				continue
157			}
158			rolesStr = strings.Replace(rolesStr, role, d.RenderRoleLinkWithChip(role), -1)
159		}
160
161		s += ufmt.Sprintf("| %s | %s | %s | [View](%s:%s/%s) |\n",
162			displayName,
163			RenderAddressLink(item.Key),
164			rolesStr,
165			linkPath, "member", item.Key)
166	}
167
168	s += "\n" + page.Picker(path) + "\n"
169	return s
170}
171
172func (d *DAOPrivate) RenderProposalsTable(path string, proposals *daokit.ProposalsStore) string {
173	pkgPath := d.Realm.PkgPath()
174	linkPath := getLinkPath(pkgPath)
175	proposalsCount := proposals.Tree.Size()
176	const pageSize = 10
177	pager := pager.NewPager(proposals.Tree, pageSize, true)
178	page := pager.MustGetPageByPath(path)
179
180	s := ""
181	if proposalsCount == 0 {
182		s += ufmt.Sprintf("\t⚠️ There are no proposals to show\n\n")
183	} else {
184		s += ufmt.Sprintf("| **ID** | **Resource** | **Proposer** | **CreatedAt** | **Status** |\n")
185		s += ufmt.Sprintf("|--------|--------------|--------------|---------------|------------|\n")
186	}
187
188	for _, item := range page.Items {
189		proposal := item.Value.(*daokit.Proposal)
190		key := item.Key
191		id, err := seqid.FromString(key)
192		if err != nil {
193			panic(err)
194		}
195		resource := d.Core.Resources.Get(proposal.Action.Type())
196		s += ufmt.Sprintf("| [%d](%s:%s/%d) | %s | %s | %s | %s |\n",
197			uint64(id), linkPath, "proposal", uint64(id),
198			resource.DisplayName,
199			RenderAddressLink(proposal.ProposerID),
200			FormatDateTimeWithUTCOffset(proposal.CreatedAt),
201			proposal.DisplayStatus().String())
202	}
203
204	s += "\n" + page.Picker(path) + "\n"
205	return s
206}