···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···10001000 }
10011001 }
1002100210031003+ if concreteType == "" {
10041004+ concreteType = "null"
10051005+ }
10061006+10031007 format := db.ValueTypeFormatAny
10041008 if valueFormat == "did" {
10051009 format = db.ValueTypeFormatDid
···10161020 Rkey: tid.TID(),
10171021 Name: name,
10181022 ValueType: valueType,
10191019- Scope: syntax.NSID(scope),
10231023+ Scope: scope,
10201024 Color: &color,
10211025 Multiple: multiple,
10221026 Created: time.Now(),
···10721076 Val: &repoRecord,
10731077 },
10741078 })
10791079+ if err != nil {
10801080+ fail("Failed to update labels for repo.", err)
10811081+ return
10821082+ }
1075108310761084 tx, err := rp.db.BeginTx(r.Context(), nil)
10771085 if err != nil {
···11181126 rp.pages.HxRefresh(w)
11191127}
1120112811211121-func (rp *Repo) DeleteLabel(w http.ResponseWriter, r *http.Request) {
11291129+func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) {
11221130 user := rp.oauth.GetUser(r)
11231131 l := rp.logger.With("handler", "DeleteLabel")
11241132 l = l.With("did", user.Did)
···1229123712301238func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) {
12311239 user := rp.oauth.GetUser(r)
12321232- l := rp.logger.With("handler", "DeleteLabel")
12401240+ l := rp.logger.With("handler", "SubscribeLabel")
12331241 l = l.With("did", user.Did)
12341242 l = l.With("handle", user.Handle)
12351243···12391247 return
12401248 }
1241124912421242- errorId := "label-operation"
12501250+ errorId := "default-label-operation"
12431251 fail := func(msg string, err error) {
12441252 l.Error(msg, "err", err)
12451253 rp.pages.Notice(w, errorId, msg)
···1292130012931301func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) {
12941302 user := rp.oauth.GetUser(r)
12951295- l := rp.logger.With("handler", "DeleteLabel")
13031303+ l := rp.logger.With("handler", "UnsubscribeLabel")
12961304 l = l.With("did", user.Did)
12971305 l = l.With("handle", user.Handle)
12981306···13021310 return
13031311 }
1304131213051305- errorId := "label-operation"
13131313+ errorId := "default-label-operation"
13061314 fail := func(msg string, err error) {
13071315 l.Error(msg, "err", err)
13081316 rp.pages.Notice(w, errorId, msg)
···13611369 rp.pages.HxRefresh(w)
13621370}
1363137113721372+func (rp *Repo) LabelPanel(w http.ResponseWriter, r *http.Request) {
13731373+ l := rp.logger.With("handler", "LabelPanel")
13741374+13751375+ f, err := rp.repoResolver.Resolve(r)
13761376+ if err != nil {
13771377+ l.Error("failed to get repo and knot", "err", err)
13781378+ return
13791379+ }
13801380+13811381+ subjectStr := r.FormValue("subject")
13821382+ subject, err := syntax.ParseATURI(subjectStr)
13831383+ if err != nil {
13841384+ l.Error("failed to get repo and knot", "err", err)
13851385+ return
13861386+ }
13871387+13881388+ labelDefs, err := db.GetLabelDefinitions(
13891389+ rp.db,
13901390+ db.FilterIn("at_uri", f.Repo.Labels),
13911391+ db.FilterContains("scope", subject.Collection().String()),
13921392+ )
13931393+ if err != nil {
13941394+ log.Println("failed to fetch label defs", err)
13951395+ return
13961396+ }
13971397+13981398+ defs := make(map[string]*db.LabelDefinition)
13991399+ for _, l := range labelDefs {
14001400+ defs[l.AtUri().String()] = &l
14011401+ }
14021402+14031403+ states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
14041404+ if err != nil {
14051405+ log.Println("failed to build label state", err)
14061406+ return
14071407+ }
14081408+ state := states[subject]
14091409+14101410+ user := rp.oauth.GetUser(r)
14111411+ rp.pages.LabelPanel(w, pages.LabelPanelParams{
14121412+ LoggedInUser: user,
14131413+ RepoInfo: f.RepoInfo(user),
14141414+ Defs: defs,
14151415+ Subject: subject.String(),
14161416+ State: state,
14171417+ })
14181418+}
14191419+14201420+func (rp *Repo) EditLabelPanel(w http.ResponseWriter, r *http.Request) {
14211421+ l := rp.logger.With("handler", "EditLabelPanel")
14221422+14231423+ f, err := rp.repoResolver.Resolve(r)
14241424+ if err != nil {
14251425+ l.Error("failed to get repo and knot", "err", err)
14261426+ return
14271427+ }
14281428+14291429+ subjectStr := r.FormValue("subject")
14301430+ subject, err := syntax.ParseATURI(subjectStr)
14311431+ if err != nil {
14321432+ l.Error("failed to get repo and knot", "err", err)
14331433+ return
14341434+ }
14351435+14361436+ labelDefs, err := db.GetLabelDefinitions(
14371437+ rp.db,
14381438+ db.FilterIn("at_uri", f.Repo.Labels),
14391439+ db.FilterContains("scope", subject.Collection().String()),
14401440+ )
14411441+ if err != nil {
14421442+ log.Println("failed to fetch labels", err)
14431443+ return
14441444+ }
14451445+14461446+ defs := make(map[string]*db.LabelDefinition)
14471447+ for _, l := range labelDefs {
14481448+ defs[l.AtUri().String()] = &l
14491449+ }
14501450+14511451+ states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
14521452+ if err != nil {
14531453+ log.Println("failed to build label state", err)
14541454+ return
14551455+ }
14561456+ state := states[subject]
14571457+14581458+ user := rp.oauth.GetUser(r)
14591459+ rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
14601460+ LoggedInUser: user,
14611461+ RepoInfo: f.RepoInfo(user),
14621462+ Defs: defs,
14631463+ Subject: subject.String(),
14641464+ State: state,
14651465+ })
14661466+}
14671467+13641468func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
13651469 user := rp.oauth.GetUser(r)
13661470 l := rp.logger.With("handler", "AddCollaborator")
···17901894 return
17911895 }
1792189618971897+ defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", db.DefaultLabelDefs()))
18981898+ if err != nil {
18991899+ log.Println("failed to fetch labels", err)
19001900+ rp.pages.Error503(w)
19011901+ return
19021902+ }
19031903+17931904 labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
17941905 if err != nil {
17951906 log.Println("failed to fetch labels", err)
17961907 rp.pages.Error503(w)
17971908 return
17981909 }
19101910+ // remove default labels from the labels list, if present
19111911+ defaultLabelMap := make(map[string]bool)
19121912+ for _, dl := range defaultLabels {
19131913+ defaultLabelMap[dl.AtUri().String()] = true
19141914+ }
19151915+ n := 0
19161916+ for _, l := range labels {
19171917+ if !defaultLabelMap[l.AtUri().String()] {
19181918+ labels[n] = l
19191919+ n++
19201920+ }
19211921+ }
19221922+ labels = labels[:n]
19231923+19241924+ subscribedLabels := make(map[string]struct{})
19251925+ for _, l := range f.Repo.Labels {
19261926+ subscribedLabels[l] = struct{}{}
19271927+ }
1799192818001929 rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
18011801- LoggedInUser: user,
18021802- RepoInfo: f.RepoInfo(user),
18031803- Branches: result.Branches,
18041804- Labels: labels,
18051805- Tabs: settingsTabs,
18061806- Tab: "general",
19301930+ LoggedInUser: user,
19311931+ RepoInfo: f.RepoInfo(user),
19321932+ Branches: result.Branches,
19331933+ Labels: labels,
19341934+ DefaultLabels: defaultLabels,
19351935+ SubscribedLabels: subscribedLabels,
19361936+ Tabs: settingsTabs,
19371937+ Tab: "general",
18071938 })
18081939}
18091940
+36-12
appview/validator/label.go
···1818 // Color should be a valid hex color
1919 colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
2020 // You can only label issues and pulls presently
2121- 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 format
4343+ if label.ValueType.IsNull() && label.ValueType.IsEnum() {
4344 return fmt.Errorf("null type cannot be used in conjunction with enum type")
4445 }
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 integers
5454+ 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")
6060+ }
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
···116134func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
117135 valueType := labelDef.ValueType
118136137137+ // this is permitted, it "unsets" a label
138138+ if labelOp.OperandValue == "" {
139139+ labelOp.Operation = db.LabelOperationDel
140140+ return nil
141141+ }
142142+119143 switch valueType.Type {
120144 case db.ConcreteTypeNull:
121145 // For null type, value should be empty
···125149126150 case db.ConcreteTypeString:
127151 // For string type, validate enum constraints if present
128128- if valueType.IsEnumType() {
152152+ if valueType.IsEnum() {
129153 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
130154 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
131155 }
···153177 return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
154178 }
155179156156- if valueType.IsEnumType() {
180180+ if valueType.IsEnum() {
157181 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
158182 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
159183 }
···165189 }
166190167191 // validate enum constraints if present (though uncommon for booleans)
168168- if valueType.IsEnumType() {
192192+ if valueType.IsEnum() {
169193 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
170194 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
171195 }