+1
-1
appview/db/issues.go
+1
-1
appview/db/issues.go
+1
appview/db/repos.go
+1
appview/db/repos.go
+71
-48
appview/labels/labels.go
+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
+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
+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, ¶ms.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, ¶ms.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
+1
appview/pages/repoinfo/repoinfo.go
+7
-34
appview/pages/templates/repo/issues/issue.html
+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
+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>