[MM-48031] Work template read API (#21661)
Этот коммит содержится в:
@@ -110,6 +110,20 @@ jobs:
|
|||||||
cd mattermost-server
|
cd mattermost-server
|
||||||
make app-layers
|
make app-layers
|
||||||
if [[ -n $(git status --porcelain) ]]; then echo "Please update the app layers using make app-layers"; exit 1; fi
|
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:
|
check-go-mod-tidy:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/go:1.18
|
- image: cimg/go:1.18
|
||||||
@@ -501,6 +515,9 @@ workflows:
|
|||||||
- check-app-layers:
|
- check-app-layers:
|
||||||
requires:
|
requires:
|
||||||
- setup-multi-product-repositories
|
- setup-multi-product-repositories
|
||||||
|
- check-generate-worktemplates:
|
||||||
|
requires:
|
||||||
|
- setup-multi-product-repositories
|
||||||
- check-store-layers:
|
- check-store-layers:
|
||||||
requires:
|
requires:
|
||||||
- setup-multi-product-repositories
|
- setup-multi-product-repositories
|
||||||
@@ -527,6 +544,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -541,6 +559,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -558,6 +577,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -576,6 +596,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -594,6 +615,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -608,6 +630,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -660,6 +683,9 @@ workflows:
|
|||||||
- check-app-layers:
|
- check-app-layers:
|
||||||
requires:
|
requires:
|
||||||
- setup-multi-product-repositories
|
- setup-multi-product-repositories
|
||||||
|
- check-generate-worktemplates:
|
||||||
|
requires:
|
||||||
|
- setup-multi-product-repositories
|
||||||
- check-store-layers:
|
- check-store-layers:
|
||||||
requires:
|
requires:
|
||||||
- setup-multi-product-repositories
|
- setup-multi-product-repositories
|
||||||
@@ -690,6 +716,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -704,6 +731,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -721,6 +749,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -737,6 +766,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
@@ -753,6 +783,7 @@ workflows:
|
|||||||
- check-go-mod-tidy
|
- check-go-mod-tidy
|
||||||
- check-golangci-lint
|
- check-golangci-lint
|
||||||
- check-app-layers
|
- check-app-layers
|
||||||
|
- check-generate-worktemplates
|
||||||
- check-store-layers
|
- check-store-layers
|
||||||
- check-mocks
|
- check-mocks
|
||||||
- check-email-templates
|
- check-email-templates
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -349,6 +349,9 @@ telemetry-mocks: ## Creates mock files.
|
|||||||
store-layers: ## Generate layers for the store
|
store-layers: ## Generate layers for the store
|
||||||
$(GO) generate $(GOFLAGS) ./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=<>
|
new-migration: ## Creates a new migration. Run with make new-migration name=<>
|
||||||
$(GO) install github.com/mattermost/morph/cmd/morph@master
|
$(GO) install github.com/mattermost/morph/cmd/morph@master
|
||||||
@echo "Generating new migration for mysql"
|
@echo "Generating new migration for mysql"
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ type Routes struct {
|
|||||||
|
|
||||||
Usage *mux.Router // 'api/v4/usage'
|
Usage *mux.Router // 'api/v4/usage'
|
||||||
|
|
||||||
|
WorkTemplates *mux.Router // 'api/v4/worktemplates'
|
||||||
|
|
||||||
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
|
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
|
||||||
|
|
||||||
Drafts *mux.Router // 'api/v4/drafts'
|
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.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.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
|
||||||
|
|
||||||
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
|
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
|
||||||
@@ -316,6 +320,7 @@ func Init(srv *app.Server) (*API, error) {
|
|||||||
api.InitExport()
|
api.InitExport()
|
||||||
api.InitInsights()
|
api.InitInsights()
|
||||||
api.InitUsage()
|
api.InitUsage()
|
||||||
|
api.InitWorkTemplate()
|
||||||
api.InitHostedCustomer()
|
api.InitHostedCustomer()
|
||||||
api.InitDrafts()
|
api.InitDrafts()
|
||||||
if err := api.InitGraphQL(); err != nil {
|
if err := api.InitGraphQL(); err != nil {
|
||||||
|
|||||||
67
api4/worktemplates.go
Обычный файл
67
api4/worktemplates.go
Обычный файл
@@ -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
Обычный файл
110
api4/worktemplates_test.go
Обычный файл
@@ -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)
|
GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError)
|
||||||
GetWarnMetricsBot() (*model.Bot, *model.AppError)
|
GetWarnMetricsBot() (*model.Bot, *model.AppError)
|
||||||
GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *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
|
HTTPService() httpservice.HTTPService
|
||||||
Handle404(w http.ResponseWriter, r *http.Request)
|
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)
|
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
|
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) {
|
func (a *OpenTracingAppLayer) Handle404(w http.ResponseWriter, r *http.Request) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")
|
||||||
|
|||||||
52
app/worktemplates.go
Обычный файл
52
app/worktemplates.go
Обычный файл
@@ -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
Обычный файл
2
app/worktemplates/categories.yaml
Обычный файл
@@ -0,0 +1,2 @@
|
|||||||
|
- id: product_teams
|
||||||
|
name: worktemplate.category.product_teams
|
||||||
154
app/worktemplates/generator/main.go
Обычный файл
154
app/worktemplates/generator/main.go
Обычный файл
@@ -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
Обычный файл
101
app/worktemplates/generator/worktemplate.tmpl
Обычный файл
@@ -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
Обычный файл
46
app/worktemplates/templates.yaml
Обычный файл
@@ -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
Обычный файл
342
app/worktemplates/types.go
Обычный файл
@@ -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
Обычный файл
104
app/worktemplates/worktemplate_generated.go
Обычный файл
@@ -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
Обычный файл
37
app/worktemplates/worktemplates.go
Обычный файл
@@ -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
Обычный файл
125
app/worktemplates_test.go
Обычный файл
@@ -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
|
golang.org/x/tools v0.3.0
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -178,7 +179,6 @@ require (
|
|||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.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
|
lukechampine.com/uint128 v1.1.1 // indirect
|
||||||
modernc.org/cc/v3 v3.36.0 // indirect
|
modernc.org/cc/v3 v3.36.0 // indirect
|
||||||
modernc.org/ccgo/v3 v3.16.6 // 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",
|
"id": "app.webhooks.update_outgoing.app_error",
|
||||||
"translation": "Unable to update the webhook."
|
"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",
|
"id": "bleveengine.already_started.error",
|
||||||
"translation": "Bleve is already started."
|
"translation": "Bleve is already started."
|
||||||
@@ -9686,5 +9694,25 @@
|
|||||||
{
|
{
|
||||||
"id": "web.incoming_webhook.user.app_error",
|
"id": "web.incoming_webhook.user.app_error",
|
||||||
"translation": "Couldn't find the user."
|
"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)
|
defer closeBody(r)
|
||||||
return BuildResponse(r), nil
|
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
Обычный файл
71
model/worktemplate.go
Обычный файл
@@ -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"`
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user