mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-48031] Work template read API (#21661)
This commit is contained in:
@@ -110,6 +110,20 @@ jobs:
|
||||
cd mattermost-server
|
||||
make app-layers
|
||||
if [[ -n $(git status --porcelain) ]]; then echo "Please update the app layers using make app-layers"; exit 1; fi
|
||||
|
||||
check-generate-worktemplates:
|
||||
docker:
|
||||
- image: cimg/go:1.18
|
||||
resource_class: medium
|
||||
working_directory: /mnt/ramdisk
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /mnt/ramdisk
|
||||
- run:
|
||||
command: |
|
||||
cd mattermost-server
|
||||
make generate-worktemplates
|
||||
if [[ -n $(git status --porcelain) ]]; then echo "Please update the worktemplates using make generate-worktemplates"; exit 1; fi
|
||||
check-go-mod-tidy:
|
||||
docker:
|
||||
- image: cimg/go:1.18
|
||||
@@ -501,6 +515,9 @@ workflows:
|
||||
- check-app-layers:
|
||||
requires:
|
||||
- setup-multi-product-repositories
|
||||
- check-generate-worktemplates:
|
||||
requires:
|
||||
- setup-multi-product-repositories
|
||||
- check-store-layers:
|
||||
requires:
|
||||
- setup-multi-product-repositories
|
||||
@@ -527,6 +544,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -541,6 +559,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -558,6 +577,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -576,6 +596,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -594,6 +615,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -608,6 +630,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -660,6 +683,9 @@ workflows:
|
||||
- check-app-layers:
|
||||
requires:
|
||||
- setup-multi-product-repositories
|
||||
- check-generate-worktemplates:
|
||||
requires:
|
||||
- setup-multi-product-repositories
|
||||
- check-store-layers:
|
||||
requires:
|
||||
- setup-multi-product-repositories
|
||||
@@ -690,6 +716,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -704,6 +731,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -721,6 +749,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -737,6 +766,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
@@ -753,6 +783,7 @@ workflows:
|
||||
- check-go-mod-tidy
|
||||
- check-golangci-lint
|
||||
- check-app-layers
|
||||
- check-generate-worktemplates
|
||||
- check-store-layers
|
||||
- check-mocks
|
||||
- check-email-templates
|
||||
|
||||
3
Makefile
3
Makefile
@@ -349,6 +349,9 @@ telemetry-mocks: ## Creates mock files.
|
||||
store-layers: ## Generate layers for the store
|
||||
$(GO) generate $(GOFLAGS) ./store
|
||||
|
||||
generate-worktemplates: ## Generate work templates
|
||||
$(GO) generate $(GOFLAGS) ./app/worktemplates
|
||||
|
||||
new-migration: ## Creates a new migration. Run with make new-migration name=<>
|
||||
$(GO) install github.com/mattermost/morph/cmd/morph@master
|
||||
@echo "Generating new migration for mysql"
|
||||
|
||||
@@ -140,6 +140,8 @@ type Routes struct {
|
||||
|
||||
Usage *mux.Router // 'api/v4/usage'
|
||||
|
||||
WorkTemplates *mux.Router // 'api/v4/worktemplates'
|
||||
|
||||
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
|
||||
|
||||
Drafts *mux.Router // 'api/v4/drafts'
|
||||
@@ -269,6 +271,8 @@ func Init(srv *app.Server) (*API, error) {
|
||||
|
||||
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
|
||||
|
||||
api.BaseRoutes.WorkTemplates = api.BaseRoutes.APIRoot.PathPrefix("/worktemplates").Subrouter()
|
||||
|
||||
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
|
||||
|
||||
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
|
||||
@@ -316,6 +320,7 @@ func Init(srv *app.Server) (*API, error) {
|
||||
api.InitExport()
|
||||
api.InitInsights()
|
||||
api.InitUsage()
|
||||
api.InitWorkTemplate()
|
||||
api.InitHostedCustomer()
|
||||
api.InitDrafts()
|
||||
if err := api.InitGraphQL(); err != nil {
|
||||
|
||||
67
api4/worktemplates.go
Normal file
67
api4/worktemplates.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func (api *API) InitWorkTemplate() {
|
||||
api.BaseRoutes.WorkTemplates.Handle("/categories", api.APISessionRequired(needsWorkTemplateFeatureFlag(getWorkTemplateCategories))).Methods("GET")
|
||||
api.BaseRoutes.WorkTemplates.Handle("/categories/{category}/templates", api.APISessionRequired(needsWorkTemplateFeatureFlag(getWorkTemplates))).Methods("GET")
|
||||
}
|
||||
|
||||
func needsWorkTemplateFeatureFlag(h handlerFunc) handlerFunc {
|
||||
return func(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.Config().FeatureFlags.WorkTemplate {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
h(c, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func getWorkTemplateCategories(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
t := c.AppContext.GetT()
|
||||
|
||||
categories, appErr := c.App.GetWorkTemplateCategories(t)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(categories)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getWorkTemplateCategories", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func getWorkTemplates(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireCategory()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
t := c.AppContext.GetT()
|
||||
|
||||
workTemplates, appErr := c.App.GetWorkTemplates(c.Params.Category, c.App.Config().FeatureFlags.ToMap(), t)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(workTemplates)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getWorkTemplates", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
110
api4/worktemplates_test.go
Normal file
110
api4/worktemplates_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWorkTemplateCategories(t *testing.T) {
|
||||
// Setup
|
||||
cleanup := setupWorktemplateFeatureFlag(t)
|
||||
defer cleanup()
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
assert := require.New(t)
|
||||
|
||||
worktemplates.OrderedWorkTemplateCategories = []*worktemplates.WorkTemplateCategory{
|
||||
{
|
||||
ID: "test-category",
|
||||
Name: "Test Category",
|
||||
},
|
||||
{
|
||||
ID: "test-category-2",
|
||||
Name: "Test Category 2",
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
categories, _, clientErr := th.Client.GetWorktemplateCategories()
|
||||
|
||||
// Assert
|
||||
require.NoError(t, clientErr)
|
||||
require.Len(t, categories, 2)
|
||||
assert.Equal("test-category", categories[0].ID)
|
||||
assert.Equal("test-category-2", categories[1].ID)
|
||||
}
|
||||
|
||||
func TestGetWorkTemplatesByCategory(t *testing.T) {
|
||||
// Setup
|
||||
cleanup := setupWorktemplateFeatureFlag(t)
|
||||
defer cleanup()
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
assert := require.New(t)
|
||||
|
||||
worktemplates.OrderedWorkTemplateCategories = []*worktemplates.WorkTemplateCategory{
|
||||
{
|
||||
ID: "test-category",
|
||||
Name: "Test Category",
|
||||
},
|
||||
{
|
||||
ID: "test-category-2",
|
||||
Name: "Test Category 2",
|
||||
},
|
||||
}
|
||||
|
||||
worktemplates.OrderedWorkTemplates = []*worktemplates.WorkTemplate{
|
||||
{
|
||||
ID: "test-template",
|
||||
Category: "test-category",
|
||||
UseCase: "Test Template",
|
||||
},
|
||||
{
|
||||
ID: "test-template-2",
|
||||
Category: "test-category",
|
||||
UseCase: "Test Template 2",
|
||||
},
|
||||
{ // This one should not be returned because of the feature flag
|
||||
ID: "test-template-3",
|
||||
Category: "test-category",
|
||||
UseCase: "Test Template 3",
|
||||
FeatureFlag: &worktemplates.FeatureFlag{
|
||||
Name: "random-nonexistant-feature-flag",
|
||||
Value: "true",
|
||||
},
|
||||
},
|
||||
{ // this one should not be returned because of the category
|
||||
ID: "test-template-4",
|
||||
Category: "test-category-2",
|
||||
UseCase: "Test Template 4",
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
workTemplates, _, clientErr := th.Client.GetWorkTemplatesByCategory("test-category")
|
||||
|
||||
// Assert
|
||||
assert.NoError(clientErr, "error while retrieve worktemplates list")
|
||||
assert.Len(workTemplates, 2)
|
||||
assert.Equal("test-template", workTemplates[0].ID)
|
||||
assert.Equal("test-template-2", workTemplates[1].ID)
|
||||
}
|
||||
|
||||
func setupWorktemplateFeatureFlag(t *testing.T) func() {
|
||||
t.Helper()
|
||||
|
||||
oldFFValue := os.Getenv("MM_FEATUREFLAGS_WORKTEMPLATE")
|
||||
os.Setenv("MM_FEATUREFLAGS_WORKTEMPLATE", "true")
|
||||
|
||||
return func() {
|
||||
os.Setenv("MM_FEATUREFLAGS_WORKTEMPLATE", oldFFValue)
|
||||
}
|
||||
}
|
||||
@@ -850,6 +850,8 @@ type AppIface interface {
|
||||
GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError)
|
||||
GetWarnMetricsBot() (*model.Bot, *model.AppError)
|
||||
GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError)
|
||||
GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError)
|
||||
GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError)
|
||||
HTTPService() httpservice.HTTPService
|
||||
Handle404(w http.ResponseWriter, r *http.Request)
|
||||
HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError)
|
||||
|
||||
@@ -11214,6 +11214,50 @@ func (a *OpenTracingAppLayer) GetWarnMetricsStatus() (map[string]*model.WarnMetr
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplateCategories")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetWorkTemplateCategories(t)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplates")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetWorkTemplates(category, featureFlags, t)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) Handle404(w http.ResponseWriter, r *http.Request) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")
|
||||
|
||||
52
app/worktemplates.go
Normal file
52
app/worktemplates.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
)
|
||||
|
||||
func (a *App) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
|
||||
categories, err := worktemplates.ListCategories()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetWorkTemplateCategories", "app.worktemplates.get_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
modelCategories := make([]*model.WorkTemplateCategory, len(categories))
|
||||
for i := range categories {
|
||||
modelCategories[i] = &model.WorkTemplateCategory{
|
||||
ID: categories[i].ID,
|
||||
Name: t(categories[i].Name),
|
||||
}
|
||||
}
|
||||
|
||||
return modelCategories, nil
|
||||
}
|
||||
|
||||
func (a *App) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
|
||||
templates, err := worktemplates.ListByCategory(category)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetWorkTemplates", "app.worktemplates.get_templates.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// filter out templates that are not enabled by feature Flag
|
||||
enabledTemplates := []*model.WorkTemplate{}
|
||||
for _, template := range templates {
|
||||
mTemplate := template.ToModelWorkTemplate(t)
|
||||
if template.FeatureFlag == nil {
|
||||
enabledTemplates = append(enabledTemplates, mTemplate)
|
||||
continue
|
||||
}
|
||||
|
||||
if featureFlags[template.FeatureFlag.Name] == template.FeatureFlag.Value {
|
||||
enabledTemplates = append(enabledTemplates, mTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
return enabledTemplates, nil
|
||||
}
|
||||
2
app/worktemplates/categories.yaml
Normal file
2
app/worktemplates/categories.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- id: product_teams
|
||||
name: worktemplate.category.product_teams
|
||||
154
app/worktemplates/generator/main.go
Normal file
154
app/worktemplates/generator/main.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/tools/imports"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type WorkTemplateWithMD5 struct {
|
||||
worktemplates.WorkTemplate
|
||||
MD5 string
|
||||
}
|
||||
|
||||
type WorkTemplateCategoryWithMD5 struct {
|
||||
worktemplates.WorkTemplateCategory `yaml:",inline"`
|
||||
MD5 string
|
||||
}
|
||||
|
||||
func getFileContent(filename string) ([]byte, error) {
|
||||
return os.ReadFile(path.Join(filename))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// parse categories first
|
||||
dat, err := getFileContent("categories.yaml")
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to read categories.yaml"))
|
||||
}
|
||||
|
||||
h := md5.New()
|
||||
|
||||
cats := []WorkTemplateCategoryWithMD5{} // meow
|
||||
err = yaml.Unmarshal(dat, &cats)
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to unmarshal categories.yaml"))
|
||||
}
|
||||
|
||||
// validate categories
|
||||
categoryIds := map[string]struct{}{}
|
||||
for id := range cats {
|
||||
cat := cats[id]
|
||||
|
||||
if cat.ID == "" && cat.Name == "" {
|
||||
// skip empty array element
|
||||
continue
|
||||
}
|
||||
|
||||
if cat.ID == "" {
|
||||
log.Fatal(errors.New("category ID cannot be empty"))
|
||||
}
|
||||
if cat.Name == "" {
|
||||
log.Fatal(errors.New("category name cannot be empty"))
|
||||
}
|
||||
categoryIds[cat.ID] = struct{}{}
|
||||
|
||||
h.Write([]byte(cat.ID))
|
||||
cats[id].MD5 = fmt.Sprintf("%x", h.Sum(nil))
|
||||
h.Reset()
|
||||
}
|
||||
|
||||
dat, err = getFileContent("templates.yaml")
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to read templates.yaml"))
|
||||
}
|
||||
|
||||
dec := yaml.NewDecoder(bytes.NewReader(dat))
|
||||
ts := []WorkTemplateWithMD5{}
|
||||
for {
|
||||
t := worktemplates.WorkTemplate{}
|
||||
err = dec.Decode(&t)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
if t.ID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
h.Write([]byte(t.ID))
|
||||
err = t.Validate(categoryIds)
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to validate template"))
|
||||
}
|
||||
|
||||
ts = append(ts, WorkTemplateWithMD5{
|
||||
WorkTemplate: t,
|
||||
MD5: fmt.Sprintf("%x", h.Sum(nil)),
|
||||
})
|
||||
h.Reset()
|
||||
}
|
||||
|
||||
code := bytes.NewBuffer(nil)
|
||||
tmpl, err := template.New("worktemplates").Parse(tpl)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tmpl.Execute(code, struct {
|
||||
Templates []WorkTemplateWithMD5
|
||||
Categories []WorkTemplateCategoryWithMD5
|
||||
}{
|
||||
Templates: ts,
|
||||
Categories: cats,
|
||||
})
|
||||
|
||||
formattedCode, err := imports.Process(path.Join("worktemplate_generated.go"), code.Bytes(), &imports.Options{Comments: true})
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to format code"))
|
||||
}
|
||||
|
||||
err = os.WriteFile(path.Join("worktemplate_generated.go"), formattedCode, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// print all translatable content
|
||||
fmt.Println("\nTranslation helpers:\n====================")
|
||||
for _, t := range ts {
|
||||
translationHelper(t.Description.Channel)
|
||||
translationHelper(t.Description.Board)
|
||||
translationHelper(t.Description.Playbook)
|
||||
translationHelper(t.Description.Integration)
|
||||
}
|
||||
}
|
||||
|
||||
var translationHelperTemplate = `{
|
||||
"id": %q,
|
||||
"translation": %q
|
||||
},`
|
||||
|
||||
func translationHelper(t *worktemplates.TranslatableString) {
|
||||
if t != nil && t.ID != "" && t.DefaultMessage != "" {
|
||||
fmt.Printf(translationHelperTemplate, t.ID, t.DefaultMessage)
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed worktemplate.tmpl
|
||||
var tpl string
|
||||
101
app/worktemplates/generator/worktemplate.tmpl
Normal file
101
app/worktemplates/generator/worktemplate.tmpl
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make generate-worktemplates"
|
||||
// DO NOT EDIT
|
||||
|
||||
package worktemplates
|
||||
|
||||
func init() {
|
||||
{{- range .Categories}}
|
||||
registerWorkTemplateCategory("{{.ID}}", wtc{{.MD5}})
|
||||
{{- end -}}
|
||||
{{range .Templates}}
|
||||
registerWorkTemplate("{{.ID}}", wt{{.MD5}})
|
||||
{{- end}}
|
||||
|
||||
// Register categories strings
|
||||
{{range .Categories -}}
|
||||
_ = T("{{.Name}}")
|
||||
{{end}}
|
||||
|
||||
// Register translation strings
|
||||
{{range .Templates -}}
|
||||
{{if and (.Description.Channel) (ne .Description.Channel.ID "")}}_ = T("{{.Description.Channel.ID}}")
|
||||
{{end -}}
|
||||
{{if and (.Description.Board) (ne .Description.Board.ID "")}}_ = T("{{.Description.Board.ID}}")
|
||||
{{end -}}
|
||||
{{if and (.Description.Playbook) (ne .Description.Playbook.ID "")}}_ = T("{{.Description.Playbook.ID}}")
|
||||
{{end -}}
|
||||
{{if and (.Description.Integration) (ne .Description.Integration.ID "")}}_ = T("{{.Description.Integration.ID}}")
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
}
|
||||
|
||||
{{range .Categories}}
|
||||
var wtc{{.MD5}} = &WorkTemplateCategory{
|
||||
ID: "{{.ID}}",
|
||||
Name: "{{.Name}}",
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{range .Templates}}
|
||||
var wt{{.MD5}} = &WorkTemplate{
|
||||
ID: "{{.ID}}",
|
||||
Category: "{{.Category}}",
|
||||
UseCase: "{{.UseCase}}",
|
||||
Illustration: "{{.Illustration}}",
|
||||
Visibility: "{{.Visibility}}",
|
||||
{{if .FeatureFlag}}FeatureFlag: &FeatureFlag{
|
||||
Name: "{{.FeatureFlag.Name}}",
|
||||
Value: "{{.FeatureFlag.Value}}",
|
||||
},{{end}}
|
||||
Description: Description{
|
||||
{{if .Description.Channel}}Channel: &TranslatableString{
|
||||
ID: "{{.Description.Channel.ID}}",
|
||||
DefaultMessage: "{{.Description.Channel.DefaultMessage}}",
|
||||
Illustration: "{{.Description.Channel.Illustration}}",
|
||||
},{{end}}
|
||||
{{if .Description.Board}}Board: &TranslatableString{
|
||||
ID: "{{.Description.Board.ID}}",
|
||||
DefaultMessage: "{{.Description.Board.DefaultMessage}}",
|
||||
Illustration: "{{.Description.Board.Illustration}}",
|
||||
},{{end}}
|
||||
{{if .Description.Playbook}}Playbook: &TranslatableString{
|
||||
ID: "{{.Description.Playbook.ID}}",
|
||||
DefaultMessage: "{{.Description.Playbook.DefaultMessage}}",
|
||||
Illustration: "{{.Description.Playbook.Illustration}}",
|
||||
},{{end}}
|
||||
{{if .Description.Integration}}Integration: &TranslatableString{
|
||||
ID: "{{.Description.Integration.ID}}",
|
||||
DefaultMessage: "{{.Description.Integration.DefaultMessage}}",
|
||||
Illustration: "{{.Description.Integration.Illustration}}",
|
||||
},{{end}}
|
||||
},
|
||||
Content: []Content{
|
||||
{{range .Content}}{
|
||||
{{if .Channel}}Channel: &Channel{
|
||||
ID: "{{.Channel.ID}}",
|
||||
Name: "{{.Channel.Name}}",
|
||||
Purpose: "{{.Channel.Purpose}}",
|
||||
Playbook: "{{.Channel.Playbook}}",
|
||||
Illustration: "{{.Channel.Illustration}}",
|
||||
},{{end}}{{if .Board}}Board: &Board{
|
||||
ID: "{{.Board.ID}}",
|
||||
Template: "{{.Board.Template}}",
|
||||
Name: "{{.Board.Name}}",
|
||||
Channel: "{{.Board.Channel}}",
|
||||
Illustration: "{{.Board.Illustration}}",
|
||||
},{{end}}{{if .Playbook}}Playbook: &Playbook{
|
||||
Template: "{{.Playbook.Template}}",
|
||||
Name: "{{.Playbook.Name}}",
|
||||
ID: "{{.Playbook.ID}}",
|
||||
Illustration: "{{.Playbook.Illustration}}",
|
||||
},{{end}}{{if .Integration}}Integration: &Integration{
|
||||
ID: "{{.Integration.ID}}",
|
||||
},{{end}}
|
||||
},
|
||||
{{end}}
|
||||
},
|
||||
}
|
||||
{{end}}
|
||||
46
app/worktemplates/templates.yaml
Normal file
46
app/worktemplates/templates.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
id: "product_teams/feature_release:v1"
|
||||
category: product_teams
|
||||
useCase: Feature Release
|
||||
illustration: https://via.placeholder.com/204x123.png
|
||||
visibility: public
|
||||
description:
|
||||
channel:
|
||||
id: "worktemplate.product_teams.feature_release.description.channel"
|
||||
defaultMessage: "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots."
|
||||
board:
|
||||
id: "worktemplate.product_teams.feature_release.description.board"
|
||||
defaultMessage: "Use our Meeting Agenda board template for recurring meetings like standup and our Project Tasks board to manage the progress of tasks along the way."
|
||||
playbook:
|
||||
id: "worktemplate.product_teams.feature_release.description.playbook"
|
||||
defaultMessage: "Create transparent workflows across development teams to ensure your feature development process is seamless."
|
||||
integration:
|
||||
id: "worktemplate.product_teams.feature_release.description.integration"
|
||||
defaultMessage: "Increase productivity in your channel by integrating a Jira bot and Github bot. These will be downloaded for you."
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Integrations"
|
||||
content:
|
||||
- channel:
|
||||
id: feature-release
|
||||
name: Feature Release
|
||||
playbook: product-release-playbook # playbook id. if set the channel will be created by the playbook run.
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Channel+feature+release"
|
||||
- board:
|
||||
id: "board-meeting-agenda"
|
||||
template: "meeting agenda|bwps66irhr7b9dxgayf9kz33g5o" # <-- have to find a way to target the board template... could hardcode the ids but need to verify that they don't change?
|
||||
name: Meeting Agenda
|
||||
channel: feature-release # <-- optional. we use the channel "id" from above
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Board+meeting+agenda"
|
||||
- board:
|
||||
id: "board-project-task"
|
||||
template: "project task|bmttiziw35irgtmztewd9upyqdy"
|
||||
name: project task board
|
||||
channel: feature-release
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Board+project+task"
|
||||
- playbook:
|
||||
template: "product release" # <-- playbooks templates don't have ids, have to rely on name
|
||||
name: "Feature release"
|
||||
id: product-release-playbook
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Playbook+feature+release"
|
||||
- integration:
|
||||
id: jira
|
||||
- integration:
|
||||
id: github
|
||||
342
app/worktemplates/types.go
Normal file
342
app/worktemplates/types.go
Normal file
@@ -0,0 +1,342 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package worktemplates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type WorkTemplateCategory struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type WorkTemplate struct {
|
||||
ID string `yaml:"id"`
|
||||
Category string `yaml:"category"`
|
||||
UseCase string `yaml:"useCase"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
Visibility string `yaml:"visibility"`
|
||||
FeatureFlag *FeatureFlag `yaml:"featureFlag,omitempty"`
|
||||
Description Description `yaml:"description"`
|
||||
Content []Content `yaml:"content"`
|
||||
}
|
||||
|
||||
func (wt WorkTemplate) ToModelWorkTemplate(t i18n.TranslateFunc) *model.WorkTemplate {
|
||||
mwt := &model.WorkTemplate{
|
||||
ID: wt.ID,
|
||||
Category: wt.Category,
|
||||
UseCase: wt.UseCase,
|
||||
Illustration: wt.Illustration,
|
||||
Visibility: wt.Visibility,
|
||||
}
|
||||
|
||||
if wt.FeatureFlag != nil {
|
||||
mwt.FeatureFlag = &model.WorkTemplateFeatureFlag{
|
||||
Name: wt.FeatureFlag.Name,
|
||||
Value: wt.FeatureFlag.Value,
|
||||
}
|
||||
}
|
||||
|
||||
if wt.Description.Channel != nil {
|
||||
mwt.Description.Channel = &model.DescriptionContent{
|
||||
Message: wt.Description.Channel.Translate(t),
|
||||
Illustration: wt.Description.Channel.Illustration,
|
||||
}
|
||||
}
|
||||
|
||||
if wt.Description.Board != nil {
|
||||
mwt.Description.Board = &model.DescriptionContent{
|
||||
Message: wt.Description.Board.Translate(t),
|
||||
Illustration: wt.Description.Board.Illustration,
|
||||
}
|
||||
}
|
||||
|
||||
if wt.Description.Playbook != nil {
|
||||
mwt.Description.Playbook = &model.DescriptionContent{
|
||||
Message: wt.Description.Playbook.Translate(t),
|
||||
Illustration: wt.Description.Playbook.Illustration,
|
||||
}
|
||||
}
|
||||
|
||||
if wt.Description.Integration != nil {
|
||||
mwt.Description.Integration = &model.DescriptionContent{
|
||||
Message: wt.Description.Integration.Translate(t),
|
||||
Illustration: wt.Description.Integration.Illustration,
|
||||
}
|
||||
}
|
||||
|
||||
for _, content := range wt.Content {
|
||||
if content.Channel != nil {
|
||||
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
|
||||
Channel: &model.WorkTemplateChannel{
|
||||
ID: content.Channel.ID,
|
||||
Name: content.Channel.Name,
|
||||
Purpose: content.Channel.Purpose,
|
||||
Playbook: content.Channel.Playbook,
|
||||
Illustration: content.Channel.Illustration,
|
||||
},
|
||||
})
|
||||
}
|
||||
if content.Board != nil {
|
||||
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
|
||||
Board: &model.WorkTemplateBoard{
|
||||
ID: content.Board.ID,
|
||||
Name: content.Board.Name,
|
||||
Template: content.Board.Template,
|
||||
Channel: content.Board.Channel,
|
||||
Illustration: content.Board.Illustration,
|
||||
},
|
||||
})
|
||||
}
|
||||
if content.Playbook != nil {
|
||||
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
|
||||
Playbook: &model.WorkTemplatePlaybook{
|
||||
ID: content.Playbook.ID,
|
||||
Name: content.Playbook.Name,
|
||||
Template: content.Playbook.Template,
|
||||
Illustration: content.Playbook.Illustration,
|
||||
},
|
||||
})
|
||||
}
|
||||
if content.Integration != nil {
|
||||
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
|
||||
Integration: &model.WorkTemplateIntegration{
|
||||
ID: content.Integration.ID,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return mwt
|
||||
}
|
||||
|
||||
func (wt WorkTemplate) Validate(categoryIds map[string]struct{}) error {
|
||||
if wt.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if wt.Category == "" {
|
||||
return errors.New("category is required")
|
||||
}
|
||||
if _, ok := categoryIds[wt.Category]; !ok {
|
||||
return fmt.Errorf("category %s does not exist", wt.Category)
|
||||
}
|
||||
if wt.UseCase == "" {
|
||||
return errors.New("useCase is required")
|
||||
}
|
||||
if wt.Illustration == "" {
|
||||
return errors.New("illustration is required")
|
||||
}
|
||||
if wt.Visibility == "" {
|
||||
return errors.New("visibility is required")
|
||||
}
|
||||
hasChannel := false
|
||||
hasBoard := false
|
||||
hasPlaybook := false
|
||||
hasIntegration := false
|
||||
foundChannels := map[string]struct{}{}
|
||||
foundPlaybooks := map[string]struct{}{}
|
||||
foundBoards := map[string]struct{}{}
|
||||
foundIntegrations := map[string]struct{}{}
|
||||
mustHaveChannels := []string{}
|
||||
mustHavePlaybooks := []string{}
|
||||
|
||||
currentIdx := 0
|
||||
for _, content := range wt.Content {
|
||||
if content.Channel != nil {
|
||||
hasChannel = true
|
||||
if cErr := content.Channel.Validate(); cErr != nil {
|
||||
return wrapContentError(cErr, currentIdx)
|
||||
}
|
||||
if _, ok := foundChannels[content.Channel.ID]; ok {
|
||||
return wrapContentError(fmt.Errorf("duplicate channel %s found", content.Channel.ID), currentIdx)
|
||||
}
|
||||
foundChannels[content.Channel.ID] = struct{}{}
|
||||
|
||||
if content.Channel.Playbook != "" {
|
||||
mustHavePlaybooks = append(mustHavePlaybooks, content.Channel.Playbook)
|
||||
}
|
||||
}
|
||||
|
||||
if content.Board != nil {
|
||||
hasBoard = true
|
||||
if cErr := content.Board.Validate(); cErr != nil {
|
||||
return wrapContentError(cErr, currentIdx)
|
||||
}
|
||||
if _, ok := foundBoards[content.Board.ID]; ok {
|
||||
return wrapContentError(fmt.Errorf("duplicate board %s found", content.Board.ID), currentIdx)
|
||||
}
|
||||
foundBoards[content.Board.ID] = struct{}{}
|
||||
|
||||
if content.Board.Channel != "" {
|
||||
mustHaveChannels = append(mustHaveChannels, content.Board.Channel)
|
||||
}
|
||||
}
|
||||
if content.Playbook != nil {
|
||||
hasPlaybook = true
|
||||
if cErr := content.Playbook.Validate(); cErr != nil {
|
||||
return wrapContentError(cErr, currentIdx)
|
||||
}
|
||||
if _, ok := foundPlaybooks[content.Playbook.ID]; ok {
|
||||
return wrapContentError(fmt.Errorf("duplicate playbook %s found", content.Playbook.ID), currentIdx)
|
||||
}
|
||||
foundPlaybooks[content.Playbook.ID] = struct{}{}
|
||||
}
|
||||
if content.Integration != nil {
|
||||
hasIntegration = true
|
||||
if cErr := content.Integration.Validate(); cErr != nil {
|
||||
return wrapContentError(cErr, currentIdx)
|
||||
}
|
||||
if _, ok := foundIntegrations[content.Integration.ID]; ok {
|
||||
return wrapContentError(fmt.Errorf("duplicate integration %s found", content.Integration.ID), currentIdx)
|
||||
}
|
||||
foundIntegrations[content.Integration.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if hasChannel && wt.Description.Channel == nil {
|
||||
return errors.New("description.channel is required")
|
||||
}
|
||||
if hasBoard && wt.Description.Board == nil {
|
||||
return errors.New("description.board is required")
|
||||
}
|
||||
if hasPlaybook && wt.Description.Playbook == nil {
|
||||
return errors.New("description.playbook is required")
|
||||
}
|
||||
if hasIntegration && wt.Description.Integration == nil {
|
||||
return errors.New("description.integration is required")
|
||||
}
|
||||
|
||||
for _, channel := range mustHaveChannels {
|
||||
if _, ok := foundChannels[channel]; !ok {
|
||||
return fmt.Errorf("channel %s is required", channel)
|
||||
}
|
||||
}
|
||||
|
||||
for _, playbook := range mustHavePlaybooks {
|
||||
if _, ok := foundPlaybooks[playbook]; !ok {
|
||||
return fmt.Errorf("playbook %s is required", playbook)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type FeatureFlag struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type TranslatableString struct {
|
||||
ID string `yaml:"id"`
|
||||
DefaultMessage string `yaml:"defaultMessage"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
}
|
||||
|
||||
func (ts TranslatableString) Translate(t i18n.TranslateFunc) string {
|
||||
if ts.ID != "" {
|
||||
msg := t(ts.ID)
|
||||
if msg != ts.ID && msg != "" {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
return ts.DefaultMessage
|
||||
}
|
||||
|
||||
type Description struct {
|
||||
Channel *TranslatableString `yaml:"channel"`
|
||||
Board *TranslatableString `yaml:"board"`
|
||||
Playbook *TranslatableString `yaml:"playbook"`
|
||||
Integration *TranslatableString `yaml:"integration"`
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Purpose string `yaml:"purpose"`
|
||||
Playbook string `yaml:"playbook"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
}
|
||||
|
||||
func (c *Channel) Validate() error {
|
||||
if c.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if c.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Board struct {
|
||||
ID string `yaml:"id"`
|
||||
Template string `yaml:"template"`
|
||||
Name string `yaml:"name"`
|
||||
Channel string `yaml:"channel"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
}
|
||||
|
||||
func (b Board) Validate() error {
|
||||
if b.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if b.Template == "" {
|
||||
return errors.New("template is required")
|
||||
}
|
||||
if b.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Playbook struct {
|
||||
Template string `yaml:"template"`
|
||||
Name string `yaml:"name"`
|
||||
ID string `yaml:"id"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
}
|
||||
|
||||
func (p *Playbook) Validate() error {
|
||||
if p.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if p.Template == "" {
|
||||
return errors.New("template is required")
|
||||
}
|
||||
if p.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Integration struct {
|
||||
ID string `yaml:"id"`
|
||||
}
|
||||
|
||||
func (i *Integration) Validate() error {
|
||||
if i.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
Channel *Channel `yaml:"channel,omitempty"`
|
||||
Board *Board `yaml:"board,omitempty"`
|
||||
Playbook *Playbook `yaml:"playbook,omitempty"`
|
||||
Integration *Integration `yaml:"integration,omitempty"`
|
||||
}
|
||||
|
||||
func wrapContentError(err error, index int) error {
|
||||
return errors.Wrapf(err, "content #%d validation failed", index)
|
||||
}
|
||||
104
app/worktemplates/worktemplate_generated.go
Normal file
104
app/worktemplates/worktemplate_generated.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make generate-worktemplates"
|
||||
// DO NOT EDIT
|
||||
|
||||
package worktemplates
|
||||
|
||||
func init() {
|
||||
registerWorkTemplateCategory("product_teams", wtc846b565cd80043537945134a54812e07)
|
||||
registerWorkTemplate("product_teams/feature_release:v1", wt00a1b44a5831c0a3acb14787b3fdd352)
|
||||
|
||||
// Register categories strings
|
||||
_ = T("worktemplate.category.product_teams")
|
||||
|
||||
// Register translation strings
|
||||
_ = T("worktemplate.product_teams.feature_release.description.channel")
|
||||
_ = T("worktemplate.product_teams.feature_release.description.board")
|
||||
_ = T("worktemplate.product_teams.feature_release.description.playbook")
|
||||
_ = T("worktemplate.product_teams.feature_release.description.integration")
|
||||
}
|
||||
|
||||
var wtc846b565cd80043537945134a54812e07 = &WorkTemplateCategory{
|
||||
ID: "product_teams",
|
||||
Name: "worktemplate.category.product_teams",
|
||||
}
|
||||
|
||||
var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{
|
||||
ID: "product_teams/feature_release:v1",
|
||||
Category: "product_teams",
|
||||
UseCase: "Feature Release",
|
||||
Illustration: "https://via.placeholder.com/204x123.png",
|
||||
Visibility: "public",
|
||||
|
||||
Description: Description{
|
||||
Channel: &TranslatableString{
|
||||
ID: "worktemplate.product_teams.feature_release.description.channel",
|
||||
DefaultMessage: "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots.",
|
||||
Illustration: "",
|
||||
},
|
||||
Board: &TranslatableString{
|
||||
ID: "worktemplate.product_teams.feature_release.description.board",
|
||||
DefaultMessage: "Use our Meeting Agenda board template for recurring meetings like standup and our Project Tasks board to manage the progress of tasks along the way.",
|
||||
Illustration: "",
|
||||
},
|
||||
Playbook: &TranslatableString{
|
||||
ID: "worktemplate.product_teams.feature_release.description.playbook",
|
||||
DefaultMessage: "Create transparent workflows across development teams to ensure your feature development process is seamless.",
|
||||
Illustration: "",
|
||||
},
|
||||
Integration: &TranslatableString{
|
||||
ID: "worktemplate.product_teams.feature_release.description.integration",
|
||||
DefaultMessage: "Increase productivity in your channel by integrating a Jira bot and Github bot. These will be downloaded for you.",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Integrations",
|
||||
},
|
||||
},
|
||||
Content: []Content{
|
||||
{
|
||||
Channel: &Channel{
|
||||
ID: "feature-release",
|
||||
Name: "Feature Release",
|
||||
Purpose: "",
|
||||
Playbook: "product-release-playbook",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Channel+feature+release",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &Board{
|
||||
ID: "board-meeting-agenda",
|
||||
Template: "meeting agenda|bwps66irhr7b9dxgayf9kz33g5o",
|
||||
Name: "Meeting Agenda",
|
||||
Channel: "feature-release",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Board+meeting+agenda",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &Board{
|
||||
ID: "board-project-task",
|
||||
Template: "project task|bmttiziw35irgtmztewd9upyqdy",
|
||||
Name: "project task board",
|
||||
Channel: "feature-release",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Board+project+task",
|
||||
},
|
||||
},
|
||||
{
|
||||
Playbook: &Playbook{
|
||||
Template: "product release",
|
||||
Name: "Feature release",
|
||||
ID: "product-release-playbook",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Playbook+feature+release",
|
||||
},
|
||||
},
|
||||
{
|
||||
Integration: &Integration{
|
||||
ID: "jira",
|
||||
},
|
||||
},
|
||||
{
|
||||
Integration: &Integration{
|
||||
ID: "github",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
37
app/worktemplates/worktemplates.go
Normal file
37
app/worktemplates/worktemplates.go
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:generate go run generator/main.go
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package worktemplates
|
||||
|
||||
var OrderedWorkTemplates = []*WorkTemplate{}
|
||||
var OrderedWorkTemplateCategories = []*WorkTemplateCategory{}
|
||||
|
||||
// T is a placeholder to allow the translation tool to register the strings
|
||||
func T(id string) string {
|
||||
return id
|
||||
}
|
||||
|
||||
func registerWorkTemplate(id string, wt *WorkTemplate) {
|
||||
OrderedWorkTemplates = append(OrderedWorkTemplates, wt)
|
||||
}
|
||||
|
||||
func registerWorkTemplateCategory(id string, wtc *WorkTemplateCategory) {
|
||||
OrderedWorkTemplateCategories = append(OrderedWorkTemplateCategories, wtc)
|
||||
}
|
||||
|
||||
func ListCategories() ([]*WorkTemplateCategory, error) {
|
||||
return OrderedWorkTemplateCategories, nil
|
||||
}
|
||||
|
||||
func ListByCategory(category string) ([]*WorkTemplate, error) {
|
||||
wts := []*WorkTemplate{}
|
||||
for i := range OrderedWorkTemplates {
|
||||
if OrderedWorkTemplates[i].Category == category {
|
||||
wts = append(wts, OrderedWorkTemplates[i])
|
||||
}
|
||||
}
|
||||
|
||||
return wts, nil
|
||||
}
|
||||
125
app/worktemplates_test.go
Normal file
125
app/worktemplates_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
)
|
||||
|
||||
func TestGetWorkTemplateCategories(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
assert := require.New(t)
|
||||
|
||||
worktemplates.OrderedWorkTemplateCategories = wtGetCategories()
|
||||
|
||||
categories, appErr := th.App.GetWorkTemplateCategories(wtTranslationFunc)
|
||||
assert.Nil(appErr)
|
||||
assert.Len(categories, 2)
|
||||
assert.Equal("Translated test.1", categories[0].Name)
|
||||
assert.Equal("Translated test.2", categories[1].Name)
|
||||
}
|
||||
|
||||
func TestGetWorkTemplatesByCategory(t *testing.T) {
|
||||
// Setup
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
assert := require.New(t)
|
||||
|
||||
existingFFkey := "test-feature-flag"
|
||||
existingFFvalue := "true"
|
||||
ff := map[string]string{
|
||||
existingFFkey: existingFFvalue,
|
||||
}
|
||||
|
||||
worktemplates.OrderedWorkTemplateCategories = wtGetCategories()
|
||||
firstCat := worktemplates.OrderedWorkTemplateCategories[0]
|
||||
worktemplates.OrderedWorkTemplates = []*worktemplates.WorkTemplate{
|
||||
{
|
||||
ID: "test-template",
|
||||
Category: firstCat.ID,
|
||||
UseCase: "test use case",
|
||||
Description: worktemplates.Description{
|
||||
Channel: &worktemplates.TranslatableString{
|
||||
ID: "test-template-channel-description",
|
||||
DefaultMessage: "test template channel description",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // this one should not be returned because of the FF
|
||||
ID: "test-template-2",
|
||||
Category: firstCat.ID,
|
||||
UseCase: "test use case 2",
|
||||
FeatureFlag: &worktemplates.FeatureFlag{
|
||||
Name: "nonexistant-random-test-feature-flag",
|
||||
Value: "hi",
|
||||
},
|
||||
Description: worktemplates.Description{
|
||||
Channel: &worktemplates.TranslatableString{
|
||||
ID: "test-template-2-channel-description",
|
||||
DefaultMessage: "test template 2 channel description",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // this one should be present and match the FF
|
||||
ID: "test-template-3",
|
||||
Category: firstCat.ID,
|
||||
UseCase: "test use case 3",
|
||||
FeatureFlag: &worktemplates.FeatureFlag{
|
||||
Name: existingFFkey,
|
||||
Value: existingFFvalue,
|
||||
},
|
||||
Description: worktemplates.Description{
|
||||
Channel: &worktemplates.TranslatableString{
|
||||
ID: "unknown", // simulating an unknown translation, we return the default message in this case
|
||||
DefaultMessage: "default message picked for unknown",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // this one should not be returned because of the category
|
||||
ID: "test-template-4",
|
||||
Category: "cat-test2",
|
||||
UseCase: "test use case 4",
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
worktemplates, appErr := th.App.GetWorkTemplates(firstCat.ID, ff, wtTranslationFunc)
|
||||
|
||||
// Assert
|
||||
assert.Nil(appErr)
|
||||
assert.Len(worktemplates, 2)
|
||||
// assert the correct work templates have been returned
|
||||
assert.Equal("test-template", worktemplates[0].ID)
|
||||
assert.Equal("test-template-3", worktemplates[1].ID)
|
||||
// assert the descriptions have been translated
|
||||
assert.Equal("Translated test-template-channel-description", worktemplates[0].Description.Channel.Message)
|
||||
assert.Equal("default message picked for unknown", worktemplates[1].Description.Channel.Message)
|
||||
}
|
||||
|
||||
// helpers
|
||||
func wtTranslationFunc(id string, args ...interface{}) string {
|
||||
if id == "unknown" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return "Translated " + id
|
||||
}
|
||||
|
||||
func wtGetCategories() []*worktemplates.WorkTemplateCategory {
|
||||
return []*worktemplates.WorkTemplateCategory{
|
||||
{
|
||||
ID: "cat-test1",
|
||||
Name: "test.1",
|
||||
},
|
||||
{
|
||||
ID: "cat-test2",
|
||||
Name: "test.2",
|
||||
},
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -70,6 +70,7 @@ require (
|
||||
golang.org/x/tools v0.3.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -178,7 +179,6 @@ require (
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
modernc.org/cc/v3 v3.36.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||
|
||||
28
i18n/en.json
28
i18n/en.json
@@ -6991,6 +6991,14 @@
|
||||
"id": "app.webhooks.update_outgoing.app_error",
|
||||
"translation": "Unable to update the webhook."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplates.get_categories.app_error",
|
||||
"translation": "Unable to get work template categories"
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplates.get_templates.app_error",
|
||||
"translation": "Unable to get work templates"
|
||||
},
|
||||
{
|
||||
"id": "bleveengine.already_started.error",
|
||||
"translation": "Bleve is already started."
|
||||
@@ -9686,5 +9694,25 @@
|
||||
{
|
||||
"id": "web.incoming_webhook.user.app_error",
|
||||
"translation": "Couldn't find the user."
|
||||
},
|
||||
{
|
||||
"id": "worktemplate.category.product_teams",
|
||||
"translation": "Product Teams"
|
||||
},
|
||||
{
|
||||
"id": "worktemplate.product_teams.feature_release.description.board",
|
||||
"translation": "Use our Meeting Agenda board template for recurring meetings like standup and our Project Tasks board to manage the progress of tasks along the way."
|
||||
},
|
||||
{
|
||||
"id": "worktemplate.product_teams.feature_release.description.channel",
|
||||
"translation": "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots."
|
||||
},
|
||||
{
|
||||
"id": "worktemplate.product_teams.feature_release.description.integration",
|
||||
"translation": "Increase productivity in your channel by integrating a Jira bot and Github bot. These will be downloaded for you."
|
||||
},
|
||||
{
|
||||
"id": "worktemplate.product_teams.feature_release.description.playbook",
|
||||
"translation": "Create transparent workflows across development teams to ensure your feature development process is seamless."
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8557,3 +8557,34 @@ func (c *Client4) AddUserToGroupSyncables(userID string) (*Response, error) {
|
||||
defer closeBody(r)
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// Worktemplates sections
|
||||
|
||||
func (c *Client4) worktemplatesRoute() string {
|
||||
return "/worktemplates"
|
||||
}
|
||||
|
||||
// GetWorktemplateCategories returns categories of worktemplates
|
||||
func (c *Client4) GetWorktemplateCategories() ([]*WorkTemplateCategory, *Response, error) {
|
||||
r, err := c.DoAPIGet(c.worktemplatesRoute()+"/categories", "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var categories []*WorkTemplateCategory
|
||||
err = json.NewDecoder(r.Body).Decode(&categories)
|
||||
return categories, BuildResponse(r), err
|
||||
}
|
||||
|
||||
func (c *Client4) GetWorkTemplatesByCategory(category string) ([]*WorkTemplate, *Response, error) {
|
||||
r, err := c.DoAPIGet(c.worktemplatesRoute()+"/categories/"+category+"/templates", "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var templates []*WorkTemplate
|
||||
err = json.NewDecoder(r.Body).Decode(&templates)
|
||||
return templates, BuildResponse(r), err
|
||||
}
|
||||
|
||||
71
model/worktemplate.go
Normal file
71
model/worktemplate.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
type WorkTemplateCategory struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type WorkTemplate struct {
|
||||
ID string `json:"id"`
|
||||
Category string `json:"category"`
|
||||
UseCase string `json:"useCase"`
|
||||
Illustration string `json:"illustration"`
|
||||
Visibility string `json:"visibility"`
|
||||
FeatureFlag *WorkTemplateFeatureFlag `json:"featureFlag,omitempty"`
|
||||
Description Description `json:"description"`
|
||||
Content []WorkTemplateContent `json:"content"`
|
||||
}
|
||||
|
||||
type WorkTemplateFeatureFlag struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type DescriptionContent struct {
|
||||
Message string `json:"message"`
|
||||
Illustration string `json:"illustration"`
|
||||
}
|
||||
|
||||
type Description struct {
|
||||
Channel *DescriptionContent `json:"channel"`
|
||||
Board *DescriptionContent `json:"board"`
|
||||
Playbook *DescriptionContent `json:"playbook"`
|
||||
Integration *DescriptionContent `json:"integration"`
|
||||
}
|
||||
|
||||
type WorkTemplateChannel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Purpose string `json:"purpose"`
|
||||
Playbook string `json:"playbook"`
|
||||
Illustration string `json:"illustration"`
|
||||
}
|
||||
|
||||
type WorkTemplateBoard struct {
|
||||
ID string `json:"id"`
|
||||
Template string `json:"template"`
|
||||
Name string `json:"name"`
|
||||
Channel string `json:"channel"`
|
||||
Illustration string `json:"illustration"`
|
||||
}
|
||||
|
||||
type WorkTemplatePlaybook struct {
|
||||
Template string `json:"template"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Illustration string `json:"illustration"`
|
||||
}
|
||||
|
||||
type WorkTemplateIntegration struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type WorkTemplateContent struct {
|
||||
Channel *WorkTemplateChannel `json:"channel,omitempty"`
|
||||
Board *WorkTemplateBoard `json:"board,omitempty"`
|
||||
Playbook *WorkTemplatePlaybook `json:"playbook,omitempty"`
|
||||
Integration *WorkTemplateIntegration `json:"integration,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user