diff --git a/appview/db/issues.go b/appview/db/issues.go --- a/appview/db/issues.go +++ b/appview/db/issues.go @@ -174,7 +174,7 @@ func (i *IssueComment) IsTopLevel() bool { return i.ReplyTo == nil } -func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { +func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { created, err := time.Parse(time.RFC3339, record.CreatedAt) if err != nil { created = time.Now() diff --git a/appview/db/repos.go b/appview/db/repos.go --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -53,6 +53,7 @@ Description: description, CreatedAt: r.Created.Format(time.RFC3339), Source: source, Spindle: spindle, + Labels: r.Labels, } } diff --git a/appview/labels/labels.go b/appview/labels/labels.go --- a/appview/labels/labels.go +++ b/appview/labels/labels.go @@ -53,17 +53,28 @@ func (l *Labels) Router(mw *middleware.Middleware) http.Handler { r := chi.NewRouter() - r.With(middleware.AuthMiddleware(l.oauth)).Put("/perform", l.PerformLabelOp) + r.Use(middleware.AuthMiddleware(l.oauth)) + r.Put("/perform", l.PerformLabelOp) return r } +// this is a tricky handler implementation: +// - the user selects the new state of all the labels in the label panel and hits save +// - this handler should calculate the diff in order to create the labelop record +// - we need the diff in order to maintain a "history" of operations performed by users func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { user := l.oauth.GetUser(r) + noticeId := "add-label-error" + + fail := func(msg string, err error) { + l.logger.Error("failed to add label", "err", err) + l.pages.Notice(w, noticeId, msg) + } + if err := r.ParseForm(); err != nil { - l.logger.Error("failed to parse form data", "error", err) - http.Error(w, "Invalid form data", http.StatusBadRequest) + fail("Invalid form.", err) return } @@ -73,34 +84,11 @@ performedAt := time.Now() indexedAt := time.Now() repoAt := r.Form.Get("repo") subjectUri := r.Form.Get("subject") - keys := r.Form["operand-key"] - vals := r.Form["operand-val"] - - var labelOps []db.LabelOp - for i := range len(keys) { - op := r.FormValue(fmt.Sprintf("op-%d", i)) - if op == "" { - op = string(db.LabelOperationDel) - } - key := keys[i] - val := vals[i] - - labelOps = append(labelOps, db.LabelOp{ - Did: did, - Rkey: rkey, - Subject: syntax.ATURI(subjectUri), - Operation: db.LabelOperation(op), - OperandKey: key, - OperandValue: val, - PerformedAt: performedAt, - IndexedAt: indexedAt, - }) - } // find all the labels that this repo subscribes to repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt)) if err != nil { - http.Error(w, "Invalid form data", http.StatusBadRequest) + fail("Failed to get labels for this repository.", err) return } @@ -111,32 +99,71 @@ } actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts)) if err != nil { - http.Error(w, "Invalid form data", http.StatusBadRequest) + fail("Invalid form data.", err) return } - for i := range labelOps { - def := actx.Defs[labelOps[i].OperandKey] - if err := l.validator.ValidateLabelOp(def, &labelOps[i]); err != nil { - l.logger.Error("form failed to validate", "err", err) - http.Error(w, "Invalid form data", http.StatusBadRequest) - return - } - - l.logger.Info("value changed to: ", "v", labelOps[i].OperandValue) - } + l.logger.Info("actx", "labels", labelAts) + l.logger.Info("actx", "defs", actx.Defs) // calculate the start state by applying already known labels existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri)) if err != nil { - http.Error(w, "Invalid form data", http.StatusBadRequest) + fail("Invalid form data.", err) return } labelState := db.NewLabelState() actx.ApplyLabelOps(labelState, existingOps) - l.logger.Info("state", "state", labelState) + var labelOps []db.LabelOp + + // first delete all existing state + for key, vals := range labelState.Inner() { + for val := range vals { + labelOps = append(labelOps, db.LabelOp{ + Did: did, + Rkey: rkey, + Subject: syntax.ATURI(subjectUri), + Operation: db.LabelOperationDel, + OperandKey: key, + OperandValue: val, + PerformedAt: performedAt, + IndexedAt: indexedAt, + }) + } + } + + // add all the new state the user specified + for key, vals := range r.Form { + if _, ok := actx.Defs[key]; !ok { + continue + } + + for _, val := range vals { + labelOps = append(labelOps, db.LabelOp{ + Did: did, + Rkey: rkey, + Subject: syntax.ATURI(subjectUri), + Operation: db.LabelOperationAdd, + OperandKey: key, + OperandValue: val, + PerformedAt: performedAt, + IndexedAt: indexedAt, + }) + } + } + + // reduce the opset + labelOps = db.ReduceLabelOps(labelOps) + + for i := range labelOps { + def := actx.Defs[labelOps[i].OperandKey] + if err := l.validator.ValidateLabelOp(def, &labelOps[i]); err != nil { + fail(fmt.Sprintf("Invalid form data: %s", err), err) + return + } + } // next, apply all ops introduced in this request and filter out ones that are no-ops validLabelOps := labelOps[:0] @@ -157,8 +184,7 @@ record := db.LabelOpsAsRecord(validLabelOps) client, err := l.oauth.AuthorizedClient(r) if err != nil { - l.logger.Error("failed to create client", "error", err) - http.Error(w, "Invalid form data", http.StatusBadRequest) + fail("Failed to authorize user.", err) return } @@ -171,15 +197,14 @@ Val: &record, }, }) if err != nil { - l.logger.Error("failed to write to PDS", "error", err) - http.Error(w, "failed to write to PDS", http.StatusInternalServerError) + fail("Failed to create record on PDS for user.", err) return } atUri := resp.Uri tx, err := l.db.BeginTx(r.Context(), nil) if err != nil { - l.logger.Error("failed to start tx", "error", err) + fail("Failed to update labels. Try again later.", err) return } @@ -200,11 +225,9 @@ defer rollback() for _, o := range validLabelOps { if _, err := db.AddLabelOp(l.db, &o); err != nil { - l.logger.Error("failed to add op", "err", err) + fail("Failed to update labels. Try again later.", err) return } - - l.logger.Info("performed label op", "did", o.Did, "rkey", o.Rkey, "kind", o.Operation, "subjcet", o.Subject, "key", o.OperandKey) } err = tx.Commit() diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -29,11 +29,22 @@ return template.FuncMap{ "split": func(s string) []string { return strings.Split(s, "\n") }, + "trimPrefix": func(s, prefix string) string { + return strings.TrimPrefix(s, prefix) + }, "join": func(elems []string, sep string) string { return strings.Join(elems, sep) }, "contains": func(s string, target string) bool { return strings.Contains(s, target) + }, + "mapContains": func(m any, key any) bool { + mapValue := reflect.ValueOf(m) + if mapValue.Kind() != reflect.Map { + return false + } + keyValue := reflect.ValueOf(key) + return mapValue.MapIndex(keyValue).IsValid() }, "resolve": func(s string) string { identity, err := p.resolver.ResolveIdent(context.Background(), s) diff --git a/appview/pages/pages.go b/appview/pages/pages.go --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -838,13 +838,15 @@ return p.executeRepo("repo/settings", w, params) } type RepoGeneralSettingsParams struct { - LoggedInUser *oauth.User - RepoInfo repoinfo.RepoInfo - Labels []db.LabelDefinition - Active string - Tabs []map[string]any - Tab string - Branches []types.Branch + LoggedInUser *oauth.User + RepoInfo repoinfo.RepoInfo + Labels []db.LabelDefinition + DefaultLabels []db.LabelDefinition + SubscribedLabels map[string]struct{} + Active string + Tabs []map[string]any + Tab string + Branches []types.Branch } func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { @@ -1228,6 +1230,30 @@ } func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff}) +} + +type LabelPanelParams struct { + LoggedInUser *oauth.User + RepoInfo repoinfo.RepoInfo + Defs map[string]*db.LabelDefinition + Subject string + State db.LabelState +} + +func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { + return p.executePlain("repo/fragments/labelPanel", w, params) +} + +type EditLabelPanelParams struct { + LoggedInUser *oauth.User + RepoInfo repoinfo.RepoInfo + Defs map[string]*db.LabelDefinition + Subject string + State db.LabelState +} + +func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { + return p.executePlain("repo/fragments/editLabelPanel", w, params) } type PipelinesParams struct { diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go --- a/appview/pages/repoinfo/repoinfo.go +++ b/appview/pages/repoinfo/repoinfo.go @@ -52,6 +52,7 @@ } type RepoInfo struct { Name string + Rkey string OwnerDid string OwnerHandle string Description string diff --git a/appview/pages/templates/repo/issues/issue.html b/appview/pages/templates/repo/issues/issue.html --- a/appview/pages/templates/repo/issues/issue.html +++ b/appview/pages/templates/repo/issues/issue.html @@ -17,7 +17,11 @@ {{ block "repoAfter" . }}{{ end }}
No labels yet.
- {{ end }} - -