forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview/labels: change scope to be a list of NSIDs

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

oppi.li 983781b1 64db8694

verified
Changed files
+195 -35
appview
db
issues
repo
validator
+13 -8
appview/db/db.go
···
)),
value_format text not null default "any",
value_enum text, -- comma separated list
-
scope text not null,
+
scope text not null, -- comma separated list of nsid
color text,
multiple integer not null default 0,
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
···
}
}
-
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
-
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
-
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
-
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
-
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
-
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
-
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
+
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
+
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
+
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
+
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
+
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
+
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
+
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
+
func FilterLike(key string, arg any) filter { return newFilter(key, "like", arg) }
+
func FilterNotLike(key string, arg any) filter { return newFilter(key, "not like", arg) }
+
func FilterContains(key string, arg any) filter {
+
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
+
}
func (f filter) Condition() string {
rv := reflect.ValueOf(f.arg)
+1 -1
appview/issues/issues.go
···
labelDefs, err := db.GetLabelDefinitions(
rp.db,
db.FilterIn("at_uri", f.Repo.Labels),
-
db.FilterEq("scope", tangled.RepoIssueNSID),
+
db.FilterContains("scope", tangled.RepoIssueNSID),
)
if err != nil {
log.Println("failed to fetch labels", err)
+145 -14
appview/repo/repo.go
···
rp.pages.HxRefresh(w)
}
-
func (rp *Repo) AddLabel(w http.ResponseWriter, r *http.Request) {
+
func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) {
user := rp.oauth.GetUser(r)
l := rp.logger.With("handler", "AddLabel")
l = l.With("did", user.Did)
···
concreteType := r.FormValue("valueType")
valueFormat := r.FormValue("valueFormat")
enumValues := r.FormValue("enumValues")
-
scope := r.FormValue("scope")
+
scope := r.Form["scope"]
color := r.FormValue("color")
multiple := r.FormValue("multiple") == "true"
···
+
if concreteType == "" {
+
concreteType = "null"
+
}
+
format := db.ValueTypeFormatAny
if valueFormat == "did" {
format = db.ValueTypeFormatDid
···
Rkey: tid.TID(),
Name: name,
ValueType: valueType,
-
Scope: syntax.NSID(scope),
+
Scope: scope,
Color: &color,
Multiple: multiple,
Created: time.Now(),
···
Val: &repoRecord,
},
})
+
if err != nil {
+
fail("Failed to update labels for repo.", err)
+
return
+
}
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
···
rp.pages.HxRefresh(w)
-
func (rp *Repo) DeleteLabel(w http.ResponseWriter, r *http.Request) {
+
func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) {
user := rp.oauth.GetUser(r)
l := rp.logger.With("handler", "DeleteLabel")
l = l.With("did", user.Did)
···
func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) {
user := rp.oauth.GetUser(r)
-
l := rp.logger.With("handler", "DeleteLabel")
+
l := rp.logger.With("handler", "SubscribeLabel")
l = l.With("did", user.Did)
l = l.With("handle", user.Handle)
···
return
-
errorId := "label-operation"
+
errorId := "default-label-operation"
fail := func(msg string, err error) {
l.Error(msg, "err", err)
rp.pages.Notice(w, errorId, msg)
···
func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) {
user := rp.oauth.GetUser(r)
-
l := rp.logger.With("handler", "DeleteLabel")
+
l := rp.logger.With("handler", "UnsubscribeLabel")
l = l.With("did", user.Did)
l = l.With("handle", user.Handle)
···
return
-
errorId := "label-operation"
+
errorId := "default-label-operation"
fail := func(msg string, err error) {
l.Error(msg, "err", err)
rp.pages.Notice(w, errorId, msg)
···
rp.pages.HxRefresh(w)
+
func (rp *Repo) LabelPanel(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "LabelPanel")
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
subjectStr := r.FormValue("subject")
+
subject, err := syntax.ParseATURI(subjectStr)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
labelDefs, err := db.GetLabelDefinitions(
+
rp.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterContains("scope", subject.Collection().String()),
+
)
+
if err != nil {
+
log.Println("failed to fetch label defs", err)
+
return
+
}
+
+
defs := make(map[string]*db.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
+
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
+
if err != nil {
+
log.Println("failed to build label state", err)
+
return
+
}
+
state := states[subject]
+
+
user := rp.oauth.GetUser(r)
+
rp.pages.LabelPanel(w, pages.LabelPanelParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Defs: defs,
+
Subject: subject.String(),
+
State: state,
+
})
+
}
+
+
func (rp *Repo) EditLabelPanel(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "EditLabelPanel")
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
subjectStr := r.FormValue("subject")
+
subject, err := syntax.ParseATURI(subjectStr)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
labelDefs, err := db.GetLabelDefinitions(
+
rp.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterContains("scope", subject.Collection().String()),
+
)
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
return
+
}
+
+
defs := make(map[string]*db.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
+
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
+
if err != nil {
+
log.Println("failed to build label state", err)
+
return
+
}
+
state := states[subject]
+
+
user := rp.oauth.GetUser(r)
+
rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Defs: defs,
+
Subject: subject.String(),
+
State: state,
+
})
+
}
+
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
user := rp.oauth.GetUser(r)
l := rp.logger.With("handler", "AddCollaborator")
···
return
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", db.DefaultLabelDefs()))
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
rp.pages.Error503(w)
+
return
+
}
+
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
if err != nil {
log.Println("failed to fetch labels", err)
rp.pages.Error503(w)
return
+
// remove default labels from the labels list, if present
+
defaultLabelMap := make(map[string]bool)
+
for _, dl := range defaultLabels {
+
defaultLabelMap[dl.AtUri().String()] = true
+
}
+
n := 0
+
for _, l := range labels {
+
if !defaultLabelMap[l.AtUri().String()] {
+
labels[n] = l
+
n++
+
}
+
}
+
labels = labels[:n]
+
+
subscribedLabels := make(map[string]struct{})
+
for _, l := range f.Repo.Labels {
+
subscribedLabels[l] = struct{}{}
+
}
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
Branches: result.Branches,
-
Labels: labels,
-
Tabs: settingsTabs,
-
Tab: "general",
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Branches: result.Branches,
+
Labels: labels,
+
DefaultLabels: defaultLabels,
+
SubscribedLabels: subscribedLabels,
+
Tabs: settingsTabs,
+
Tab: "general",
})
+36 -12
appview/validator/label.go
···
// Color should be a valid hex color
colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
// You can only label issues and pulls presently
-
validScopes = []syntax.NSID{tangled.RepoIssueNSID, tangled.RepoPullNSID}
+
validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID}
)
func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {
···
}
if !label.ValueType.IsConcreteType() {
-
return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType)
+
return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType.Type)
}
-
if label.ValueType.IsNull() && label.ValueType.IsEnumType() {
+
// null type checks: cannot be enums, multiple or explicit format
+
if label.ValueType.IsNull() && label.ValueType.IsEnum() {
return fmt.Errorf("null type cannot be used in conjunction with enum type")
}
+
if label.ValueType.IsNull() && label.Multiple {
+
return fmt.Errorf("null type labels cannot be multiple")
+
}
+
if label.ValueType.IsNull() && !label.ValueType.IsAnyFormat() {
+
return fmt.Errorf("format cannot be used in conjunction with null type")
+
}
+
+
// format checks: cannot be used with enum, or integers
+
if !label.ValueType.IsAnyFormat() && label.ValueType.IsEnum() {
+
return fmt.Errorf("enum types cannot be used in conjunction with format specification")
+
}
+
+
if !label.ValueType.IsAnyFormat() && !label.ValueType.IsString() {
+
return fmt.Errorf("format specifications are only permitted on string types")
+
}
// validate scope (nsid format)
-
if label.Scope == "" {
+
if label.Scope == nil {
return fmt.Errorf("scope is required")
}
-
if _, err := syntax.ParseNSID(string(label.Scope)); err != nil {
-
return fmt.Errorf("failed to parse scope: %w", err)
-
}
-
if !slices.Contains(validScopes, label.Scope) {
-
return fmt.Errorf("invalid scope: scope must be one of %q", validScopes)
+
for _, s := range label.Scope {
+
if _, err := syntax.ParseNSID(s); err != nil {
+
return fmt.Errorf("failed to parse scope: %w", err)
+
}
+
if !slices.Contains(validScopes, s) {
+
return fmt.Errorf("invalid scope: scope must be present in %q", validScopes)
+
}
}
// validate color if provided
···
func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
valueType := labelDef.ValueType
+
// this is permitted, it "unsets" a label
+
if labelOp.OperandValue == "" {
+
labelOp.Operation = db.LabelOperationDel
+
return nil
+
}
+
switch valueType.Type {
case db.ConcreteTypeNull:
// For null type, value should be empty
···
case db.ConcreteTypeString:
// For string type, validate enum constraints if present
-
if valueType.IsEnumType() {
+
if valueType.IsEnum() {
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
}
···
return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
}
-
if valueType.IsEnumType() {
+
if valueType.IsEnum() {
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
}
···
}
// validate enum constraints if present (though uncommon for booleans)
-
if valueType.IsEnumType() {
+
if valueType.IsEnum() {
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
}