diff --git a/main.go b/main.go index 6a8f4e2..0027ab0 100644 --- a/main.go +++ b/main.go @@ -177,7 +177,7 @@ func main() { // MVC Handlers summaryHandler := routes.NewSummaryHandler(summaryService, userService) - settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService, mailService) + settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService) homeHandler := routes.NewHomeHandler(keyValueService) loginHandler := routes.NewLoginHandler(userService, mailService) imprintHandler := routes.NewImprintHandler(keyValueService) diff --git a/models/view/settings.go b/models/view/settings.go index 02dd846..75582d2 100644 --- a/models/view/settings.go +++ b/models/view/settings.go @@ -6,6 +6,8 @@ type SettingsViewModel struct { User *models.User LanguageMappings []*models.LanguageMapping Aliases []*SettingsVMCombinedAlias + Labels []*SettingsVMCombinedLabel + Projects []string Success string Error string } @@ -16,6 +18,11 @@ type SettingsVMCombinedAlias struct { Values []string } +type SettingsVMCombinedLabel struct { + Key string + Values []string +} + func (s *SettingsViewModel) WithSuccess(m string) *SettingsViewModel { s.Success = m return s diff --git a/routes/settings.go b/routes/settings.go index 6434eee..8df3e66 100644 --- a/routes/settings.go +++ b/routes/settings.go @@ -14,10 +14,14 @@ import ( "github.com/muety/wakapi/services/imports" "github.com/muety/wakapi/utils" "net/http" + "sort" "strconv" + "strings" "time" ) +const criticalError = "a critical error has occurred, sorry" + type SettingsHandler struct { config *conf.Config userSrvc services.IUserService @@ -26,6 +30,7 @@ type SettingsHandler struct { aliasSrvc services.IAliasService aggregationSrvc services.IAggregationService languageMappingSrvc services.ILanguageMappingService + projectLabelSrvc services.IProjectLabelService keyValueSrvc services.IKeyValueService mailSrvc services.IMailService httpClient *http.Client @@ -40,6 +45,7 @@ func NewSettingsHandler( aliasService services.IAliasService, aggregationService services.IAggregationService, languageMappingService services.ILanguageMappingService, + projectLabelService services.IProjectLabelService, keyValueService services.IKeyValueService, mailService services.IMailService, ) *SettingsHandler { @@ -49,6 +55,7 @@ func NewSettingsHandler( aliasSrvc: aliasService, aggregationSrvc: aggregationService, languageMappingSrvc: languageMappingService, + projectLabelSrvc: projectLabelService, userSrvc: userService, heartbeatSrvc: heartbeatService, keyValueSrvc: keyValueService, @@ -70,7 +77,6 @@ func (h *SettingsHandler) GetIndex(w http.ResponseWriter, r *http.Request) { if h.config.IsDev() { loadTemplates() } - templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r)) } @@ -128,6 +134,10 @@ func (h *SettingsHandler) dispatchAction(action string) action { return h.actionDeleteAlias case "add_alias": return h.actionAddAlias + case "add_label": + return h.actionAddLabel + case "delete_label": + return h.actionDeleteLabel case "delete_mapping": return h.actionDeleteLanguageMapping case "add_mapping": @@ -314,6 +324,59 @@ func (h *SettingsHandler) actionAddAlias(w http.ResponseWriter, r *http.Request) return http.StatusOK, "alias added successfully", "" } +func (h *SettingsHandler) actionAddLabel(w http.ResponseWriter, r *http.Request) (int, string, string) { + if h.config.IsDev() { + loadTemplates() + } + user := middlewares.GetPrincipal(r) + + label := &models.ProjectLabel{ + UserID: user.ID, + ProjectKey: r.PostFormValue("key"), + Label: r.PostFormValue("value"), + } + + if !label.IsValid() { + return http.StatusBadRequest, "", "invalid input" + } + + if _, err := h.projectLabelSrvc.Create(label); err != nil { + // TODO: distinguish between bad request, conflict and server error + return http.StatusBadRequest, "", "invalid input" + } + + return http.StatusOK, "label added successfully", "" +} + +func (h *SettingsHandler) actionDeleteLabel(w http.ResponseWriter, r *http.Request) (int, string, string) { + if h.config.IsDev() { + loadTemplates() + } + + user := middlewares.GetPrincipal(r) + labelKey := r.PostFormValue("key") + labelValue := r.PostFormValue("value") + + labelMap, err := h.projectLabelSrvc.GetByUserGrouped(user.ID) + if err != nil { + return http.StatusInternalServerError, "", "could not delete label" + } + + if projectLabels, ok := labelMap[labelKey]; ok { + for _, l := range projectLabels { + if l.Label == labelValue { + if err := h.projectLabelSrvc.Delete(l); err != nil { + return http.StatusInternalServerError, "", "could not delete label" + } + return http.StatusOK, "label deleted successfully", "" + } + } + return http.StatusNotFound, "", "label not found" + } else { + return http.StatusNotFound, "", "project not found" + } +} + func (h *SettingsHandler) actionDeleteLanguageMapping(w http.ResponseWriter, r *http.Request) (int, string, string) { if h.config.IsDev() { loadTemplates() @@ -554,8 +617,16 @@ func (h *SettingsHandler) regenerateSummaries(user *models.User) error { func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewModel { user := middlewares.GetPrincipal(r) + + // mappings mappings, _ := h.languageMappingSrvc.GetByUser(user.ID) - aliases, _ := h.aliasSrvc.GetByUser(user.ID) + + // aliases + aliases, err := h.aliasSrvc.GetByUser(user.ID) + if err != nil { + conf.Log().Request(r).Error("error while building alias map - %v", err) + return &view.SettingsViewModel{Error: criticalError} + } aliasMap := make(map[string][]*models.Alias) for _, a := range aliases { k := fmt.Sprintf("%s_%d", a.Key, a.Type) @@ -579,10 +650,42 @@ func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewMode combinedAliases = append(combinedAliases, ca) } + // labels + labelMap, err := h.projectLabelSrvc.GetByUserGrouped(user.ID) + if err != nil { + conf.Log().Request(r).Error("error while building settings project label map - %v", err) + return &view.SettingsViewModel{Error: criticalError} + } + + combinedLabels := make([]*view.SettingsVMCombinedLabel, 0) + for _, l := range labelMap { + cl := &view.SettingsVMCombinedLabel{ + Key: l[0].ProjectKey, + Values: make([]string, len(l)), + } + for i, l1 := range l { + cl.Values[i] = l1.Label + } + combinedLabels = append(combinedLabels, cl) + } + sort.Slice(combinedLabels, func(i, j int) bool { + return strings.Compare(combinedLabels[i].Key, combinedLabels[j].Key) < 0 + }) + + // projects + projects, err := h.heartbeatSrvc.GetEntitySetByUser(models.SummaryProject, user) + if err != nil { + conf.Log().Request(r).Error("error while fetching projects - %v", err) + return &view.SettingsViewModel{Error: criticalError} + } + sort.Strings(projects) + return &view.SettingsViewModel{ User: user, LanguageMappings: mappings, Aliases: combinedAliases, + Labels: combinedLabels, + Projects: projects, Success: r.URL.Query().Get("success"), Error: r.URL.Query().Get("error"), } diff --git a/services/project_label.go b/services/project_label.go index 18f3de5..418a682 100644 --- a/services/project_label.go +++ b/services/project_label.go @@ -40,16 +40,21 @@ func (srv *ProjectLabelService) GetByUser(userId string) ([]*models.ProjectLabel return labels, nil } -func (srv *ProjectLabelService) ResolveByUser(userId string) (map[string]string, error) { - labels := make(map[string]string) +func (srv *ProjectLabelService) GetByUserGrouped(userId string) (map[string][]*models.ProjectLabel, error) { + labels := make(map[string][]*models.ProjectLabel) userLabels, err := srv.GetByUser(userId) if err != nil { return nil, err } - for _, m := range userLabels { - labels[m.ProjectKey] = m.Label + for _, l := range userLabels { + if _, ok := labels[l.ProjectKey]; !ok { + labels[l.ProjectKey] = []*models.ProjectLabel{l} + } else { + labels[l.ProjectKey] = append(labels[l.ProjectKey], l) + } } + return labels, nil } diff --git a/services/services.go b/services/services.go index 724e855..1f7506a 100644 --- a/services/services.go +++ b/services/services.go @@ -57,7 +57,7 @@ type ILanguageMappingService interface { type IProjectLabelService interface { GetById(uint) (*models.ProjectLabel, error) GetByUser(string) ([]*models.ProjectLabel, error) - ResolveByUser(string) (map[string]string, error) + GetByUserGrouped(string) (map[string][]*models.ProjectLabel, error) Create(*models.ProjectLabel) (*models.ProjectLabel, error) Delete(mapping *models.ProjectLabel) error } diff --git a/views/settings.tpl.html b/views/settings.tpl.html index 98cb526..809c54a 100644 --- a/views/settings.tpl.html +++ b/views/settings.tpl.html @@ -203,6 +203,73 @@ +
+ +

+ Project Labels +

+
+
+
+ You can assign labels (aka. tags) to projects to group them together, e.g. by private and work. +
+ + {{ if .Labels }} +

Labels

+ {{ range $i, $label := .Labels }} +
+
+ ▸ {{ $label.Key }}: + {{ range $j, $value := $label.Values }} +
+ + + + {{- $value -}} + +
+ {{ if lt $j (add (len $label.Values) -1) }} + {{- ", " | capitalize -}} + {{ end }} + {{ end }} +
+
+ {{end}} +
+ {{end}} + + {{ if .Projects }} +

Add Label

+
+ +
+ Project: + + Label: + +
+ +
+
+
+ {{ else }} +
You don't have any projects, yet. Start out by sending a few heartbeats before you can then assign labels.
+ {{ end }} +
+
+

{ if (!tz) tz = 'Local'