···966966 rp.pages.HxRefresh(w)967967}968968969969-func (rp *Repo) AddLabel(w http.ResponseWriter, r *http.Request) {969969+func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) {970970 user := rp.oauth.GetUser(r)971971 l := rp.logger.With("handler", "AddLabel")972972 l = l.With("did", user.Did)···989989 concreteType := r.FormValue("valueType")990990 valueFormat := r.FormValue("valueFormat")991991 enumValues := r.FormValue("enumValues")992992- scope := r.FormValue("scope")992992+ scope := r.Form["scope"]993993 color := r.FormValue("color")994994 multiple := r.FormValue("multiple") == "true"995995···998998 if part = strings.TrimSpace(part); part != "" {999999 variants = append(variants, part)10001000 }10011001+ }10021002+10031003+ if concreteType == "" {10041004+ concreteType = "null"10011005 }1002100610031007 format := db.ValueTypeFormatAny···10201016 Rkey: tid.TID(),10211017 Name: name,10221018 ValueType: valueType,10231023- Scope: syntax.NSID(scope),10191019+ Scope: scope,10241020 Color: &color,10251021 Multiple: multiple,10261022 Created: time.Now(),···10761072 Val: &repoRecord,10771073 },10781074 })10751075+ if err != nil {10761076+ fail("Failed to update labels for repo.", err)10771077+ return10781078+ }1079107910801080 tx, err := rp.db.BeginTx(r.Context(), nil)10811081 if err != nil {···11261118 rp.pages.HxRefresh(w)11271119}1128112011291129-func (rp *Repo) DeleteLabel(w http.ResponseWriter, r *http.Request) {11211121+func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) {11301122 user := rp.oauth.GetUser(r)11311123 l := rp.logger.With("handler", "DeleteLabel")11321124 l = l.With("did", user.Did)···1237122912381230func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) {12391231 user := rp.oauth.GetUser(r)12401240- l := rp.logger.With("handler", "DeleteLabel")12321232+ l := rp.logger.With("handler", "SubscribeLabel")12411233 l = l.With("did", user.Did)12421234 l = l.With("handle", user.Handle)12431235···12471239 return12481240 }1249124112501250- errorId := "label-operation"12421242+ errorId := "default-label-operation"12511243 fail := func(msg string, err error) {12521244 l.Error(msg, "err", err)12531245 rp.pages.Notice(w, errorId, msg)···1300129213011293func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) {13021294 user := rp.oauth.GetUser(r)13031303- l := rp.logger.With("handler", "DeleteLabel")12951295+ l := rp.logger.With("handler", "UnsubscribeLabel")13041296 l = l.With("did", user.Did)13051297 l = l.With("handle", user.Handle)13061298···13101302 return13111303 }1312130413131313- errorId := "label-operation"13051305+ errorId := "default-label-operation"13141306 fail := func(msg string, err error) {13151307 l.Error(msg, "err", err)13161308 rp.pages.Notice(w, errorId, msg)···1367135913681360 // everything succeeded13691361 rp.pages.HxRefresh(w)13621362+}13631363+13641364+func (rp *Repo) LabelPanel(w http.ResponseWriter, r *http.Request) {13651365+ l := rp.logger.With("handler", "LabelPanel")13661366+13671367+ f, err := rp.repoResolver.Resolve(r)13681368+ if err != nil {13691369+ l.Error("failed to get repo and knot", "err", err)13701370+ return13711371+ }13721372+13731373+ subjectStr := r.FormValue("subject")13741374+ subject, err := syntax.ParseATURI(subjectStr)13751375+ if err != nil {13761376+ l.Error("failed to get repo and knot", "err", err)13771377+ return13781378+ }13791379+13801380+ labelDefs, err := db.GetLabelDefinitions(13811381+ rp.db,13821382+ db.FilterIn("at_uri", f.Repo.Labels),13831383+ db.FilterContains("scope", subject.Collection().String()),13841384+ )13851385+ if err != nil {13861386+ log.Println("failed to fetch label defs", err)13871387+ return13881388+ }13891389+13901390+ defs := make(map[string]*db.LabelDefinition)13911391+ for _, l := range labelDefs {13921392+ defs[l.AtUri().String()] = &l13931393+ }13941394+13951395+ states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))13961396+ if err != nil {13971397+ log.Println("failed to build label state", err)13981398+ return13991399+ }14001400+ state := states[subject]14011401+14021402+ user := rp.oauth.GetUser(r)14031403+ rp.pages.LabelPanel(w, pages.LabelPanelParams{14041404+ LoggedInUser: user,14051405+ RepoInfo: f.RepoInfo(user),14061406+ Defs: defs,14071407+ Subject: subject.String(),14081408+ State: state,14091409+ })14101410+}14111411+14121412+func (rp *Repo) EditLabelPanel(w http.ResponseWriter, r *http.Request) {14131413+ l := rp.logger.With("handler", "EditLabelPanel")14141414+14151415+ f, err := rp.repoResolver.Resolve(r)14161416+ if err != nil {14171417+ l.Error("failed to get repo and knot", "err", err)14181418+ return14191419+ }14201420+14211421+ subjectStr := r.FormValue("subject")14221422+ subject, err := syntax.ParseATURI(subjectStr)14231423+ if err != nil {14241424+ l.Error("failed to get repo and knot", "err", err)14251425+ return14261426+ }14271427+14281428+ labelDefs, err := db.GetLabelDefinitions(14291429+ rp.db,14301430+ db.FilterIn("at_uri", f.Repo.Labels),14311431+ db.FilterContains("scope", subject.Collection().String()),14321432+ )14331433+ if err != nil {14341434+ log.Println("failed to fetch labels", err)14351435+ return14361436+ }14371437+14381438+ defs := make(map[string]*db.LabelDefinition)14391439+ for _, l := range labelDefs {14401440+ defs[l.AtUri().String()] = &l14411441+ }14421442+14431443+ states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))14441444+ if err != nil {14451445+ log.Println("failed to build label state", err)14461446+ return14471447+ }14481448+ state := states[subject]14491449+14501450+ user := rp.oauth.GetUser(r)14511451+ rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{14521452+ LoggedInUser: user,14531453+ RepoInfo: f.RepoInfo(user),14541454+ Defs: defs,14551455+ Subject: subject.String(),14561456+ State: state,14571457+ })13701458}1371145913721460func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {···18941790 return18951791 }1896179218971897- labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))17931793+ defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", db.DefaultLabelDefs()))18981794 if err != nil {18991795 log.Println("failed to fetch labels", err)19001796 rp.pages.Error503(w)19011797 return19021798 }1903179918001800+ labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))18011801+ if err != nil {18021802+ log.Println("failed to fetch labels", err)18031803+ rp.pages.Error503(w)18041804+ return18051805+ }18061806+ // remove default labels from the labels list, if present18071807+ defaultLabelMap := make(map[string]bool)18081808+ for _, dl := range defaultLabels {18091809+ defaultLabelMap[dl.AtUri().String()] = true18101810+ }18111811+ n := 018121812+ for _, l := range labels {18131813+ if !defaultLabelMap[l.AtUri().String()] {18141814+ labels[n] = l18151815+ n++18161816+ }18171817+ }18181818+ labels = labels[:n]18191819+18201820+ subscribedLabels := make(map[string]struct{})18211821+ for _, l := range f.Repo.Labels {18221822+ subscribedLabels[l] = struct{}{}18231823+ }18241824+19041825 rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{19051905- LoggedInUser: user,19061906- RepoInfo: f.RepoInfo(user),19071907- Branches: result.Branches,19081908- Labels: labels,19091909- Tabs: settingsTabs,19101910- Tab: "general",18261826+ LoggedInUser: user,18271827+ RepoInfo: f.RepoInfo(user),18281828+ Branches: result.Branches,18291829+ Labels: labels,18301830+ DefaultLabels: defaultLabels,18311831+ SubscribedLabels: subscribedLabels,18321832+ Tabs: settingsTabs,18331833+ Tab: "general",19111834 })19121835}19131836
+36-12
appview/validator/label.go
···1818 // Color should be a valid hex color1919 colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)2020 // You can only label issues and pulls presently2121- validScopes = []syntax.NSID{tangled.RepoIssueNSID, tangled.RepoPullNSID}2121+ validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID}2222)23232424func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {···3636 }37373838 if !label.ValueType.IsConcreteType() {3939- return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType)3939+ return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType.Type)4040 }41414242- if label.ValueType.IsNull() && label.ValueType.IsEnumType() {4242+ // null type checks: cannot be enums, multiple or explicit format4343+ if label.ValueType.IsNull() && label.ValueType.IsEnum() {4344 return fmt.Errorf("null type cannot be used in conjunction with enum type")4545+ }4646+ if label.ValueType.IsNull() && label.Multiple {4747+ return fmt.Errorf("null type labels cannot be multiple")4848+ }4949+ if label.ValueType.IsNull() && !label.ValueType.IsAnyFormat() {5050+ return fmt.Errorf("format cannot be used in conjunction with null type")5151+ }5252+5353+ // format checks: cannot be used with enum, or integers5454+ if !label.ValueType.IsAnyFormat() && label.ValueType.IsEnum() {5555+ return fmt.Errorf("enum types cannot be used in conjunction with format specification")5656+ }5757+5858+ if !label.ValueType.IsAnyFormat() && !label.ValueType.IsString() {5959+ return fmt.Errorf("format specifications are only permitted on string types")4460 }45614662 // validate scope (nsid format)4747- if label.Scope == "" {6363+ if label.Scope == nil {4864 return fmt.Errorf("scope is required")4965 }5050- if _, err := syntax.ParseNSID(string(label.Scope)); err != nil {5151- return fmt.Errorf("failed to parse scope: %w", err)5252- }5353- if !slices.Contains(validScopes, label.Scope) {5454- return fmt.Errorf("invalid scope: scope must be one of %q", validScopes)6666+ for _, s := range label.Scope {6767+ if _, err := syntax.ParseNSID(s); err != nil {6868+ return fmt.Errorf("failed to parse scope: %w", err)6969+ }7070+ if !slices.Contains(validScopes, s) {7171+ return fmt.Errorf("invalid scope: scope must be present in %q", validScopes)7272+ }5573 }56745775 // validate color if provided···134116func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {135117 valueType := labelDef.ValueType136118119119+ // this is permitted, it "unsets" a label120120+ if labelOp.OperandValue == "" {121121+ labelOp.Operation = db.LabelOperationDel122122+ return nil123123+ }124124+137125 switch valueType.Type {138126 case db.ConcreteTypeNull:139127 // For null type, value should be empty···149125150126 case db.ConcreteTypeString:151127 // For string type, validate enum constraints if present152152- if valueType.IsEnumType() {128128+ if valueType.IsEnum() {153129 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {154130 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)155131 }···177153 return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)178154 }179155180180- if valueType.IsEnumType() {156156+ if valueType.IsEnum() {181157 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {182158 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)183159 }···189165 }190166191167 // validate enum constraints if present (though uncommon for booleans)192192- if valueType.IsEnumType() {168168+ if valueType.IsEnum() {193169 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {194170 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)195171 }