forked from tangled.org/core
Monorepo for Tangled

appvie: switch to new label panel interface

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li ac6cb315 d0cc53d6

verified
Changed files
+129 -97
appview
db
labels
pages
repoinfo
templates
repo
+1 -1
appview/db/issues.go
··· 174 return i.ReplyTo == nil 175 } 176 177 - func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { 178 created, err := time.Parse(time.RFC3339, record.CreatedAt) 179 if err != nil { 180 created = time.Now()
··· 174 return i.ReplyTo == nil 175 } 176 177 + func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { 178 created, err := time.Parse(time.RFC3339, record.CreatedAt) 179 if err != nil { 180 created = time.Now()
+1
appview/db/repos.go
··· 53 CreatedAt: r.Created.Format(time.RFC3339), 54 Source: source, 55 Spindle: spindle, 56 } 57 } 58
··· 53 CreatedAt: r.Created.Format(time.RFC3339), 54 Source: source, 55 Spindle: spindle, 56 + Labels: r.Labels, 57 } 58 } 59
+71 -48
appview/labels/labels.go
··· 53 func (l *Labels) Router(mw *middleware.Middleware) http.Handler { 54 r := chi.NewRouter() 55 56 - r.With(middleware.AuthMiddleware(l.oauth)).Put("/perform", l.PerformLabelOp) 57 58 return r 59 } 60 61 func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 62 user := l.oauth.GetUser(r) 63 64 if err := r.ParseForm(); err != nil { 65 - l.logger.Error("failed to parse form data", "error", err) 66 - http.Error(w, "Invalid form data", http.StatusBadRequest) 67 return 68 } 69 ··· 73 indexedAt := time.Now() 74 repoAt := r.Form.Get("repo") 75 subjectUri := r.Form.Get("subject") 76 - keys := r.Form["operand-key"] 77 - vals := r.Form["operand-val"] 78 - 79 - var labelOps []db.LabelOp 80 - for i := range len(keys) { 81 - op := r.FormValue(fmt.Sprintf("op-%d", i)) 82 - if op == "" { 83 - op = string(db.LabelOperationDel) 84 - } 85 - key := keys[i] 86 - val := vals[i] 87 - 88 - labelOps = append(labelOps, db.LabelOp{ 89 - Did: did, 90 - Rkey: rkey, 91 - Subject: syntax.ATURI(subjectUri), 92 - Operation: db.LabelOperation(op), 93 - OperandKey: key, 94 - OperandValue: val, 95 - PerformedAt: performedAt, 96 - IndexedAt: indexedAt, 97 - }) 98 - } 99 100 // find all the labels that this repo subscribes to 101 repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt)) 102 if err != nil { 103 - http.Error(w, "Invalid form data", http.StatusBadRequest) 104 return 105 } 106 ··· 111 112 actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts)) 113 if err != nil { 114 - http.Error(w, "Invalid form data", http.StatusBadRequest) 115 return 116 } 117 118 - for i := range labelOps { 119 - def := actx.Defs[labelOps[i].OperandKey] 120 - if err := l.validator.ValidateLabelOp(def, &labelOps[i]); err != nil { 121 - l.logger.Error("form failed to validate", "err", err) 122 - http.Error(w, "Invalid form data", http.StatusBadRequest) 123 - return 124 - } 125 - 126 - l.logger.Info("value changed to: ", "v", labelOps[i].OperandValue) 127 - } 128 129 // calculate the start state by applying already known labels 130 existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri)) 131 if err != nil { 132 - http.Error(w, "Invalid form data", http.StatusBadRequest) 133 return 134 } 135 136 labelState := db.NewLabelState() 137 actx.ApplyLabelOps(labelState, existingOps) 138 139 - l.logger.Info("state", "state", labelState) 140 141 // next, apply all ops introduced in this request and filter out ones that are no-ops 142 validLabelOps := labelOps[:0] ··· 157 158 client, err := l.oauth.AuthorizedClient(r) 159 if err != nil { 160 - l.logger.Error("failed to create client", "error", err) 161 - http.Error(w, "Invalid form data", http.StatusBadRequest) 162 return 163 } 164 ··· 171 }, 172 }) 173 if err != nil { 174 - l.logger.Error("failed to write to PDS", "error", err) 175 - http.Error(w, "failed to write to PDS", http.StatusInternalServerError) 176 return 177 } 178 atUri := resp.Uri 179 180 tx, err := l.db.BeginTx(r.Context(), nil) 181 if err != nil { 182 - l.logger.Error("failed to start tx", "error", err) 183 return 184 } 185 ··· 200 201 for _, o := range validLabelOps { 202 if _, err := db.AddLabelOp(l.db, &o); err != nil { 203 - l.logger.Error("failed to add op", "err", err) 204 return 205 } 206 - 207 - l.logger.Info("performed label op", "did", o.Did, "rkey", o.Rkey, "kind", o.Operation, "subjcet", o.Subject, "key", o.OperandKey) 208 } 209 210 err = tx.Commit()
··· 53 func (l *Labels) Router(mw *middleware.Middleware) http.Handler { 54 r := chi.NewRouter() 55 56 + r.Use(middleware.AuthMiddleware(l.oauth)) 57 + r.Put("/perform", l.PerformLabelOp) 58 59 return r 60 } 61 62 + // this is a tricky handler implementation: 63 + // - the user selects the new state of all the labels in the label panel and hits save 64 + // - this handler should calculate the diff in order to create the labelop record 65 + // - we need the diff in order to maintain a "history" of operations performed by users 66 func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 67 user := l.oauth.GetUser(r) 68 69 + noticeId := "add-label-error" 70 + 71 + fail := func(msg string, err error) { 72 + l.logger.Error("failed to add label", "err", err) 73 + l.pages.Notice(w, noticeId, msg) 74 + } 75 + 76 if err := r.ParseForm(); err != nil { 77 + fail("Invalid form.", err) 78 return 79 } 80 ··· 84 indexedAt := time.Now() 85 repoAt := r.Form.Get("repo") 86 subjectUri := r.Form.Get("subject") 87 88 // find all the labels that this repo subscribes to 89 repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt)) 90 if err != nil { 91 + fail("Failed to get labels for this repository.", err) 92 return 93 } 94 ··· 99 100 actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts)) 101 if err != nil { 102 + fail("Invalid form data.", err) 103 return 104 } 105 106 + l.logger.Info("actx", "labels", labelAts) 107 + l.logger.Info("actx", "defs", actx.Defs) 108 109 // calculate the start state by applying already known labels 110 existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri)) 111 if err != nil { 112 + fail("Invalid form data.", err) 113 return 114 } 115 116 labelState := db.NewLabelState() 117 actx.ApplyLabelOps(labelState, existingOps) 118 119 + var labelOps []db.LabelOp 120 + 121 + // first delete all existing state 122 + for key, vals := range labelState.Inner() { 123 + for val := range vals { 124 + labelOps = append(labelOps, db.LabelOp{ 125 + Did: did, 126 + Rkey: rkey, 127 + Subject: syntax.ATURI(subjectUri), 128 + Operation: db.LabelOperationDel, 129 + OperandKey: key, 130 + OperandValue: val, 131 + PerformedAt: performedAt, 132 + IndexedAt: indexedAt, 133 + }) 134 + } 135 + } 136 + 137 + // add all the new state the user specified 138 + for key, vals := range r.Form { 139 + if _, ok := actx.Defs[key]; !ok { 140 + continue 141 + } 142 + 143 + for _, val := range vals { 144 + labelOps = append(labelOps, db.LabelOp{ 145 + Did: did, 146 + Rkey: rkey, 147 + Subject: syntax.ATURI(subjectUri), 148 + Operation: db.LabelOperationAdd, 149 + OperandKey: key, 150 + OperandValue: val, 151 + PerformedAt: performedAt, 152 + IndexedAt: indexedAt, 153 + }) 154 + } 155 + } 156 + 157 + // reduce the opset 158 + labelOps = db.ReduceLabelOps(labelOps) 159 + 160 + for i := range labelOps { 161 + def := actx.Defs[labelOps[i].OperandKey] 162 + if err := l.validator.ValidateLabelOp(def, &labelOps[i]); err != nil { 163 + fail(fmt.Sprintf("Invalid form data: %s", err), err) 164 + return 165 + } 166 + } 167 168 // next, apply all ops introduced in this request and filter out ones that are no-ops 169 validLabelOps := labelOps[:0] ··· 184 185 client, err := l.oauth.AuthorizedClient(r) 186 if err != nil { 187 + fail("Failed to authorize user.", err) 188 return 189 } 190 ··· 197 }, 198 }) 199 if err != nil { 200 + fail("Failed to create record on PDS for user.", err) 201 return 202 } 203 atUri := resp.Uri 204 205 tx, err := l.db.BeginTx(r.Context(), nil) 206 if err != nil { 207 + fail("Failed to update labels. Try again later.", err) 208 return 209 } 210 ··· 225 226 for _, o := range validLabelOps { 227 if _, err := db.AddLabelOp(l.db, &o); err != nil { 228 + fail("Failed to update labels. Try again later.", err) 229 return 230 } 231 } 232 233 err = tx.Commit()
+11
appview/pages/funcmap.go
··· 29 "split": func(s string) []string { 30 return strings.Split(s, "\n") 31 }, 32 "join": func(elems []string, sep string) string { 33 return strings.Join(elems, sep) 34 }, 35 "contains": func(s string, target string) bool { 36 return strings.Contains(s, target) 37 }, 38 "resolve": func(s string) string { 39 identity, err := p.resolver.ResolveIdent(context.Background(), s)
··· 29 "split": func(s string) []string { 30 return strings.Split(s, "\n") 31 }, 32 + "trimPrefix": func(s, prefix string) string { 33 + return strings.TrimPrefix(s, prefix) 34 + }, 35 "join": func(elems []string, sep string) string { 36 return strings.Join(elems, sep) 37 }, 38 "contains": func(s string, target string) bool { 39 return strings.Contains(s, target) 40 + }, 41 + "mapContains": func(m any, key any) bool { 42 + mapValue := reflect.ValueOf(m) 43 + if mapValue.Kind() != reflect.Map { 44 + return false 45 + } 46 + keyValue := reflect.ValueOf(key) 47 + return mapValue.MapIndex(keyValue).IsValid() 48 }, 49 "resolve": func(s string) string { 50 identity, err := p.resolver.ResolveIdent(context.Background(), s)
+33 -7
appview/pages/pages.go
··· 838 } 839 840 type RepoGeneralSettingsParams struct { 841 - LoggedInUser *oauth.User 842 - RepoInfo repoinfo.RepoInfo 843 - Labels []db.LabelDefinition 844 - Active string 845 - Tabs []map[string]any 846 - Tab string 847 - Branches []types.Branch 848 } 849 850 func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { ··· 1228 1229 func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1230 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1231 } 1232 1233 type PipelinesParams struct {
··· 838 } 839 840 type RepoGeneralSettingsParams struct { 841 + LoggedInUser *oauth.User 842 + RepoInfo repoinfo.RepoInfo 843 + Labels []db.LabelDefinition 844 + DefaultLabels []db.LabelDefinition 845 + SubscribedLabels map[string]struct{} 846 + Active string 847 + Tabs []map[string]any 848 + Tab string 849 + Branches []types.Branch 850 } 851 852 func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { ··· 1230 1231 func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1232 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1233 + } 1234 + 1235 + type LabelPanelParams struct { 1236 + LoggedInUser *oauth.User 1237 + RepoInfo repoinfo.RepoInfo 1238 + Defs map[string]*db.LabelDefinition 1239 + Subject string 1240 + State db.LabelState 1241 + } 1242 + 1243 + func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1244 + return p.executePlain("repo/fragments/labelPanel", w, params) 1245 + } 1246 + 1247 + type EditLabelPanelParams struct { 1248 + LoggedInUser *oauth.User 1249 + RepoInfo repoinfo.RepoInfo 1250 + Defs map[string]*db.LabelDefinition 1251 + Subject string 1252 + State db.LabelState 1253 + } 1254 + 1255 + func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1256 + return p.executePlain("repo/fragments/editLabelPanel", w, params) 1257 } 1258 1259 type PipelinesParams struct {
+1
appview/pages/repoinfo/repoinfo.go
··· 52 53 type RepoInfo struct { 54 Name string 55 OwnerDid string 56 OwnerHandle string 57 Description string
··· 52 53 type RepoInfo struct { 54 Name string 55 + Rkey string 56 OwnerDid string 57 OwnerHandle string 58 Description string
+7 -34
appview/pages/templates/repo/issues/issue.html
··· 17 {{ block "repoAfter" . }}{{ end }} 18 </div> 19 <div class="col-span-1 md:col-span-2 flex flex-col gap-6"> 20 - {{ template "issueLabels" . }} 21 {{ template "issueParticipants" . }} 22 </div> 23 </div> ··· 118 </div> 119 {{ end }} 120 121 - {{ define "issueLabels" }} 122 - <div> 123 - <div class="text-sm py-1 flex items-center gap-2 font-bold text-gray-500 dark:text-gray-400 capitalize"> 124 - Labels 125 - <button 126 - class="inline-flex text-gray-500 dark:text-gray-400 {{ if not (or .RepoInfo.Roles.IsOwner .RepoInfo.Roles.IsCollaborator) }}hidden{{ end }}" 127 - popovertarget="add-label-modal" 128 - popovertargetaction="toggle"> 129 - {{ i "plus" "size-4" }} 130 - </button> 131 - </div> 132 - <div class="flex gap-1 items-center flex-wrap"> 133 - {{ range $k, $valset := $.Issue.Labels.Inner }} 134 - {{ $d := index $.LabelDefs $k }} 135 - {{ range $v, $s := $valset }} 136 - {{ template "labels/fragments/label" (dict "def" $d "val" $v) }} 137 - {{ end }} 138 - {{ else }} 139 - <p class="text-gray-500 dark:text-gray-400 ">No labels yet.</p> 140 - {{ end }} 141 - 142 - <div 143 - id="add-label-modal" 144 - popover 145 - class="bg-white w-full sm:w-[30rem] dark:bg-gray-800 p-6 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50"> 146 - {{ template "repo/fragments/addLabelModal" (dict "root" $ "subject" $.Issue.AtUri.String "state" $.Issue.Labels) }} 147 - </div> 148 - </div> 149 - </div> 150 - {{ end }} 151 - 152 {{ define "issueParticipants" }} 153 {{ $all := .Issue.Participants }} 154 {{ $ps := take $all 5 }} ··· 157 <span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span> 158 <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span> 159 </div> 160 - <div class="flex items-center -space-x-2 mt-2"> 161 {{ $c := "z-50 z-40 z-30 z-20 z-10" }} 162 {{ range $i, $p := $ps }} 163 <img 164 src="{{ tinyAvatar . }}" 165 alt="" 166 - class="rounded-full h-8 w-8 mr-1 border-2 border-gray-300 dark:border-gray-700 z-{{sub 5 $i}}0" 167 /> 168 {{ end }} 169
··· 17 {{ block "repoAfter" . }}{{ end }} 18 </div> 19 <div class="col-span-1 md:col-span-2 flex flex-col gap-6"> 20 + {{ template "repo/fragments/labelPanel" 21 + (dict "RepoInfo" $.RepoInfo 22 + "Defs" $.LabelDefs 23 + "Subject" $.Issue.AtUri 24 + "State" $.Issue.Labels) }} 25 {{ template "issueParticipants" . }} 26 </div> 27 </div> ··· 122 </div> 123 {{ end }} 124 125 {{ define "issueParticipants" }} 126 {{ $all := .Issue.Participants }} 127 {{ $ps := take $all 5 }} ··· 130 <span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span> 131 <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span> 132 </div> 133 + <div class="flex items-center -space-x-3 mt-2"> 134 {{ $c := "z-50 z-40 z-30 z-20 z-10" }} 135 {{ range $i, $p := $ps }} 136 <img 137 src="{{ tinyAvatar . }}" 138 alt="" 139 + class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0" 140 /> 141 {{ end }} 142
+4 -7
appview/pages/templates/repo/issues/issues.html
··· 80 <a href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a> 81 </span> 82 83 - {{ if .Labels.Inner }} 84 - <span class="before:content-['·']"></span> 85 - {{ range $k, $valset := .Labels.Inner }} 86 - {{ $d := index $.LabelDefs $k }} 87 - {{ range $v, $s := $valset }} 88 - {{ template "labels/fragments/label" (dict "def" $d "val" $v) }} 89 - {{ end }} 90 {{ end }} 91 {{ end }} 92 </div>
··· 80 <a href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a> 81 </span> 82 83 + {{ $state := .Labels }} 84 + {{ range $k, $d := $.LabelDefs }} 85 + {{ range $v, $s := $state.GetValSet $d.AtUri.String }} 86 + {{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }} 87 {{ end }} 88 {{ end }} 89 </div>