appview/labels: add "subscribe all" button for default labels #597

merged
opened by oppi.li targeting master from push-lxxtrqtnnoxy

quickly subscribe to all default labels.

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

Changed files
+187 -37
appview
labels
pages
templates
repo
settings
repo
state
+80
appview/ingester.go
··· 5 5 "encoding/json" 6 6 "fmt" 7 7 "log/slog" 8 + "maps" 9 + "slices" 8 10 9 11 "time" 10 12 ··· 80 82 err = i.ingestIssueComment(e) 81 83 case tangled.LabelDefinitionNSID: 82 84 err = i.ingestLabelDefinition(e) 85 + case tangled.LabelOpNSID: 86 + err = i.ingestLabelOp(e) 83 87 } 84 88 l = i.Logger.With("nsid", e.Commit.Collection) 85 89 } ··· 953 957 954 958 return nil 955 959 } 960 + 961 + func (i *Ingester) ingestLabelOp(e *jmodels.Event) error { 962 + did := e.Did 963 + rkey := e.Commit.RKey 964 + 965 + var err error 966 + 967 + l := i.Logger.With("handler", "ingestLabelOp", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 968 + l.Info("ingesting record") 969 + 970 + ddb, ok := i.Db.Execer.(*db.DB) 971 + if !ok { 972 + return fmt.Errorf("failed to index label op, invalid db cast") 973 + } 974 + 975 + switch e.Commit.Operation { 976 + case jmodels.CommitOperationCreate: 977 + raw := json.RawMessage(e.Commit.Record) 978 + record := tangled.LabelOp{} 979 + err = json.Unmarshal(raw, &record) 980 + if err != nil { 981 + return fmt.Errorf("invalid record: %w", err) 982 + } 983 + 984 + subject := syntax.ATURI(record.Subject) 985 + collection := subject.Collection() 986 + 987 + var repo *models.Repo 988 + switch collection { 989 + case tangled.RepoIssueNSID: 990 + i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject)) 991 + if err != nil || len(i) != 1 { 992 + return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i)) 993 + } 994 + repo = i[0].Repo 995 + default: 996 + return fmt.Errorf("unsupport label subject: %s", collection) 997 + } 998 + 999 + actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels)) 1000 + if err != nil { 1001 + return fmt.Errorf("failed to build label application ctx: %w", err) 1002 + } 1003 + 1004 + ops := models.LabelOpsFromRecord(did, rkey, record) 1005 + 1006 + for _, o := range ops { 1007 + def, ok := actx.Defs[o.OperandKey] 1008 + if !ok { 1009 + return fmt.Errorf("failed to find label def for key: %s, expected: %q", o.OperandKey, slices.Collect(maps.Keys(actx.Defs))) 1010 + } 1011 + if err := i.Validator.ValidateLabelOp(def, &o); err != nil { 1012 + return fmt.Errorf("failed to validate labelop: %w", err) 1013 + } 1014 + } 1015 + 1016 + tx, err := ddb.Begin() 1017 + if err != nil { 1018 + return err 1019 + } 1020 + defer tx.Rollback() 1021 + 1022 + for _, o := range ops { 1023 + _, err = db.AddLabelOp(tx, &o) 1024 + if err != nil { 1025 + return fmt.Errorf("failed to add labelop: %w", err) 1026 + } 1027 + } 1028 + 1029 + if err = tx.Commit(); err != nil { 1030 + return err 1031 + } 1032 + } 1033 + 1034 + return nil 1035 + }
-3
appview/labels/labels.go
··· 104 104 return 105 105 } 106 106 107 - l.logger.Info("actx", "labels", labelAts) 108 - l.logger.Info("actx", "defs", actx.Defs) 109 - 110 107 // calculate the start state by applying already known labels 111 108 existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri)) 112 109 if err != nil {
+10 -9
appview/pages/pages.go
··· 834 834 } 835 835 836 836 type RepoGeneralSettingsParams struct { 837 - LoggedInUser *oauth.User 838 - RepoInfo repoinfo.RepoInfo 839 - Labels []models.LabelDefinition 840 - DefaultLabels []models.LabelDefinition 841 - SubscribedLabels map[string]struct{} 842 - Active string 843 - Tabs []map[string]any 844 - Tab string 845 - Branches []types.Branch 837 + LoggedInUser *oauth.User 838 + RepoInfo repoinfo.RepoInfo 839 + Labels []models.LabelDefinition 840 + DefaultLabels []models.LabelDefinition 841 + SubscribedLabels map[string]struct{} 842 + ShouldSubscribeAll bool 843 + Active string 844 + Tabs []map[string]any 845 + Tab string 846 + Branches []types.Branch 846 847 } 847 848 848 849 func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error {
+36 -6
appview/pages/templates/repo/settings/general.html
··· 46 46 47 47 {{ define "defaultLabelSettings" }} 48 48 <div class="flex flex-col gap-2"> 49 - <h2 class="text-sm pb-2 uppercase font-bold">Default Labels</h2> 50 - <p class="text-gray-500 dark:text-gray-400"> 51 - Manage your issues and pulls by creating labels to categorize them. Only 52 - repository owners may configure labels. You may choose to subscribe to 53 - default labels, or create entirely custom labels. 54 - </p> 49 + <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center"> 50 + <div class="col-span-1 md:col-span-2"> 51 + <h2 class="text-sm pb-2 uppercase font-bold">Default Labels</h2> 52 + <p class="text-gray-500 dark:text-gray-400"> 53 + Manage your issues and pulls by creating labels to categorize them. Only 54 + repository owners may configure labels. You may choose to subscribe to 55 + default labels, or create entirely custom labels. 56 + <p> 57 + </div> 58 + <form class="col-span-1 md:col-span-1 md:justify-self-end"> 59 + {{ $title := "Unubscribe from all labels" }} 60 + {{ $icon := "x" }} 61 + {{ $text := "unsubscribe all" }} 62 + {{ $action := "unsubscribe" }} 63 + {{ if $.ShouldSubscribeAll }} 64 + {{ $title = "Subscribe to all labels" }} 65 + {{ $icon = "check-check" }} 66 + {{ $text = "subscribe all" }} 67 + {{ $action = "subscribe" }} 68 + {{ end }} 69 + {{ range .DefaultLabels }} 70 + <input type="hidden" name="label" value="{{ .AtUri.String }}"> 71 + {{ end }} 72 + <button 73 + type="submit" 74 + title="{{$title}}" 75 + class="btn flex items-center gap-2 group" 76 + hx-swap="none" 77 + hx-post="/{{ $.RepoInfo.FullName }}/settings/label/{{$action}}" 78 + {{ if not .RepoInfo.Roles.IsOwner }}disabled{{ end }}> 79 + {{ i $icon "size-4" }} 80 + {{ $text }} 81 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 82 + </button> 83 + </form> 84 + </div> 55 85 <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 w-full"> 56 86 {{ range .DefaultLabels }} 57 87 <div id="label-{{.Id}}" class="flex items-center justify-between p-2 pl-4">
+60 -19
appview/repo/repo.go
··· 1248 1248 return 1249 1249 } 1250 1250 1251 + if err := r.ParseForm(); err != nil { 1252 + l.Error("invalid form", "err", err) 1253 + return 1254 + } 1255 + 1251 1256 errorId := "default-label-operation" 1252 1257 fail := func(msg string, err error) { 1253 1258 l.Error(msg, "err", err) 1254 1259 rp.pages.Notice(w, errorId, msg) 1255 1260 } 1256 1261 1257 - labelAt := r.FormValue("label") 1258 - _, err = db.GetLabelDefinition(rp.db, db.FilterEq("at_uri", labelAt)) 1262 + labelAts := r.Form["label"] 1263 + _, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts)) 1259 1264 if err != nil { 1260 1265 fail("Failed to subscribe to label.", err) 1261 1266 return 1262 1267 } 1263 1268 1264 1269 newRepo := f.Repo 1265 - newRepo.Labels = append(newRepo.Labels, labelAt) 1270 + newRepo.Labels = append(newRepo.Labels, labelAts...) 1271 + 1272 + // dedup 1273 + slices.Sort(newRepo.Labels) 1274 + newRepo.Labels = slices.Compact(newRepo.Labels) 1275 + 1266 1276 repoRecord := newRepo.AsRecord() 1267 1277 1268 1278 client, err := rp.oauth.AuthorizedClient(r) ··· 1286 1296 }, 1287 1297 }) 1288 1298 1289 - err = db.SubscribeLabel(rp.db, &models.RepoLabel{ 1290 - RepoAt: f.RepoAt(), 1291 - LabelAt: syntax.ATURI(labelAt), 1292 - }) 1299 + tx, err := rp.db.Begin() 1293 1300 if err != nil { 1294 1301 fail("Failed to subscribe to label.", err) 1295 1302 return 1296 1303 } 1304 + defer tx.Rollback() 1305 + 1306 + for _, l := range labelAts { 1307 + err = db.SubscribeLabel(tx, &models.RepoLabel{ 1308 + RepoAt: f.RepoAt(), 1309 + LabelAt: syntax.ATURI(l), 1310 + }) 1311 + if err != nil { 1312 + fail("Failed to subscribe to label.", err) 1313 + return 1314 + } 1315 + } 1316 + 1317 + if err := tx.Commit(); err != nil { 1318 + fail("Failed to subscribe to label.", err) 1319 + return 1320 + } 1297 1321 1298 1322 // everything succeeded 1299 1323 rp.pages.HxRefresh(w) ··· 1311 1335 return 1312 1336 } 1313 1337 1338 + if err := r.ParseForm(); err != nil { 1339 + l.Error("invalid form", "err", err) 1340 + return 1341 + } 1342 + 1314 1343 errorId := "default-label-operation" 1315 1344 fail := func(msg string, err error) { 1316 1345 l.Error(msg, "err", err) 1317 1346 rp.pages.Notice(w, errorId, msg) 1318 1347 } 1319 1348 1320 - labelAt := r.FormValue("label") 1321 - _, err = db.GetLabelDefinition(rp.db, db.FilterEq("at_uri", labelAt)) 1349 + labelAts := r.Form["label"] 1350 + _, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts)) 1322 1351 if err != nil { 1323 1352 fail("Failed to unsubscribe to label.", err) 1324 1353 return ··· 1328 1357 newRepo := f.Repo 1329 1358 var updated []string 1330 1359 for _, l := range newRepo.Labels { 1331 - if l != labelAt { 1360 + if !slices.Contains(labelAts, l) { 1332 1361 updated = append(updated, l) 1333 1362 } 1334 1363 } ··· 1359 1388 err = db.UnsubscribeLabel( 1360 1389 rp.db, 1361 1390 db.FilterEq("repo_at", f.RepoAt()), 1362 - db.FilterEq("label_at", labelAt), 1391 + db.FilterIn("label_at", labelAts), 1363 1392 ) 1364 1393 if err != nil { 1365 1394 fail("Failed to unsubscribe label.", err) ··· 1927 1956 subscribedLabels[l] = struct{}{} 1928 1957 } 1929 1958 1959 + // if there is atleast 1 unsubbed default label, show the "subscribe all" button, 1960 + // if all default labels are subbed, show the "unsubscribe all" button 1961 + shouldSubscribeAll := false 1962 + for _, dl := range defaultLabels { 1963 + if _, ok := subscribedLabels[dl.AtUri().String()]; !ok { 1964 + // one of the default labels is not subscribed to 1965 + shouldSubscribeAll = true 1966 + break 1967 + } 1968 + } 1969 + 1930 1970 rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{ 1931 - LoggedInUser: user, 1932 - RepoInfo: f.RepoInfo(user), 1933 - Branches: result.Branches, 1934 - Labels: labels, 1935 - DefaultLabels: defaultLabels, 1936 - SubscribedLabels: subscribedLabels, 1937 - Tabs: settingsTabs, 1938 - Tab: "general", 1971 + LoggedInUser: user, 1972 + RepoInfo: f.RepoInfo(user), 1973 + Branches: result.Branches, 1974 + Labels: labels, 1975 + DefaultLabels: defaultLabels, 1976 + SubscribedLabels: subscribedLabels, 1977 + ShouldSubscribeAll: shouldSubscribeAll, 1978 + Tabs: settingsTabs, 1979 + Tab: "general", 1939 1980 }) 1940 1981 } 1941 1982
+1
appview/state/state.go
··· 103 103 tangled.RepoIssueNSID, 104 104 tangled.RepoIssueCommentNSID, 105 105 tangled.LabelDefinitionNSID, 106 + tangled.LabelOpNSID, 106 107 }, 107 108 nil, 108 109 slog.Default(),