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}