diff --git a/appview/db/label.go b/appview/db/label.go index d8a82301..ed782e49 100644 --- a/appview/db/label.go +++ b/appview/db/label.go @@ -77,7 +77,7 @@ func (vt ValueType) IsBool() bool { return vt.Type == ConcreteTypeBool } -func (vt ValueType) IsEnumType() bool { +func (vt ValueType) IsEnum() bool { return len(vt.Enum) > 0 } @@ -631,8 +631,24 @@ func (s LabelState) ContainsLabel(l string) bool { return false } -func (s *LabelState) GetValSet(l string) set { - return s.inner[l] +// go maps behavior in templates make this necessary, +// indexing a map and getting `set` in return is apparently truthy +func (s LabelState) ContainsLabelAndVal(l, v string) bool { + if valset, exists := s.inner[l]; exists { + if _, exists := valset[v]; exists { + return true + } + } + + return false +} + +func (s LabelState) GetValSet(l string) set { + if valset, exists := s.inner[l]; exists { + return valset + } else { + return make(set) + } } type LabelApplicationCtx struct { @@ -719,3 +735,55 @@ func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) { _ = c.ApplyLabelOp(state, o) } } + +// IsInverse checks if one label operation is the inverse of another +// returns true if one is an add and the other is a delete with the same key and value +func (op1 LabelOp) IsInverse(op2 LabelOp) bool { + if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue { + return false + } + + return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) || + (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd) +} + +// removes pairs of label operations that are inverses of each other +// from the given slice. the function preserves the order of remaining operations. +func ReduceLabelOps(ops []LabelOp) []LabelOp { + if len(ops) <= 1 { + return ops + } + + keep := make([]bool, len(ops)) + for i := range keep { + keep[i] = true + } + + for i := range ops { + if !keep[i] { + continue + } + + for j := i + 1; j < len(ops); j++ { + if !keep[j] { + continue + } + + if ops[i].IsInverse(ops[j]) { + keep[i] = false + keep[j] = false + break // move to next i since this one is now eliminated + } + } + } + + // build result slice with only kept operations + var result []LabelOp + for i, op := range ops { + if keep[i] { + result = append(result, op) + } + } + + return result +} diff --git a/appview/db/repos.go b/appview/db/repos.go index 432b3da5..c6ad0221 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -53,6 +53,7 @@ func (r *Repo) AsRecord() tangled.Repo { CreatedAt: r.Created.Format(time.RFC3339), Source: source, Spindle: spindle, + Labels: r.Labels, } } diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 8430668d..a0bbbf65 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -108,7 +108,7 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { defs[l.AtUri().String()] = &l } - rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ + fmt.Println(rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ LoggedInUser: user, RepoInfo: f.RepoInfo(user), Issue: issue, @@ -117,7 +117,7 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { Reactions: reactionCountMap, UserReacted: userReactions, LabelDefs: defs, - }) + })) } func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) { diff --git a/appview/labels/labels.go b/appview/labels/labels.go index 1e63f01f..0d54d05a 100644 --- a/appview/labels/labels.go +++ b/appview/labels/labels.go @@ -53,17 +53,28 @@ func New( 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 @@ func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 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,68 @@ func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 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) - } - // 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 +181,7 @@ func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 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 +194,14 @@ func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { }, }) 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 +222,9 @@ func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 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 index 2a2bfa8a..e2973731 100644 --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -29,6 +29,9 @@ func (p *Pages) funcMap() 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) }, diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 72cc7370..ab8612fa 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -1210,6 +1210,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 { LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go index 5fa4673e..bfbb4463 100644 --- a/appview/pages/repoinfo/repoinfo.go +++ b/appview/pages/repoinfo/repoinfo.go @@ -52,6 +52,7 @@ func (r RepoInfo) GetTabs() [][]string { type RepoInfo struct { Name string + Rkey string OwnerDid string OwnerHandle string Description string diff --git a/appview/pages/templates/labels/fragments/label.html b/appview/pages/templates/labels/fragments/label.html index 386c5a73..3a6932a8 100644 --- a/appview/pages/templates/labels/fragments/label.html +++ b/appview/pages/templates/labels/fragments/label.html @@ -1,9 +1,28 @@ {{ define "labels/fragments/label" }} {{ $d := .def }} {{ $v := .val }} + {{ $withPrefix := .withPrefix }} {{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }} - {{ $d.Name }}{{ if not $d.ValueType.IsNull }}/{{ template "labelVal" (dict "def" $d "val" $v) }}{{ end }} + + {{ $lhs := printf "%s" $d.Name }} + {{ $rhs := "" }} + + {{ if not $d.ValueType.IsNull }} + {{ if $d.ValueType.IsDidFormat }} + {{ $v = resolve $v }} + {{ end }} + + {{ if not $withPrefix }} + {{ $lhs = "" }} + {{ else }} + {{ $lhs = printf "%s/" $d.Name }} + {{ end }} + + {{ $rhs = printf "%s" $v }} + {{ end }} + + {{ printf "%s%s" $lhs $rhs }} {{ end }} @@ -13,7 +32,7 @@ {{ $v := .val }} {{ if $d.ValueType.IsDidFormat }} - {{ resolve $v }} + {{ resolve $v }} {{ else }} {{ $v }} {{ end }} diff --git a/appview/pages/templates/repo/fragments/addLabelModal.html b/appview/pages/templates/repo/fragments/addLabelModal.html deleted file mode 100644 index ab9b759e..00000000 --- a/appview/pages/templates/repo/fragments/addLabelModal.html +++ /dev/null @@ -1,127 +0,0 @@ -{{ define "repo/fragments/addLabelModal" }} - {{ $root := .root }} - {{ $subject := .subject }} - {{ $state := .state }} - {{ with $root }} -
- {{ end }} -{{ end }} - -{{ define "labelCheckbox" }} - {{ $key := .key }} - {{ $val := .val }} - {{ $def := .def }} - {{ $id := .id }} - {{ $isChecked := .isChecked }} -+ No labels defined yet. You can define custom labels in settings. +
+ {{ end }} +None yet.
+ {{ end }} +None yet.
+ {{ end }} +No labels yet.
- {{ end }} - -