[MM-47846] Execute Work Template (#22055)
Этот коммит содержится в:
1
Makefile
1
Makefile
@@ -416,6 +416,7 @@ sharedchannel-mocks: ## Creates mock files for shared channels.
|
||||
misc-mocks: ## Creates mocks for misc interfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
$(GOBIN)/mockery --dir utils --name LicenseValidatorIface --output utils/mocks --note 'Regenerate this file using `make misc-mocks`.'
|
||||
$(GOBIN)/mockery --dir app --name WorkTemplateExecutor --output app/mocks --note 'Regenerate this file using `make misc-mocks`.'
|
||||
|
||||
email-mocks: ## Creates mocks for misc interfaces.
|
||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||
|
||||
147
api4/work_templates.go
Обычный файл
147
api4/work_templates.go
Обычный файл
@@ -0,0 +1,147 @@
|
||||
// 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/app/worktemplates"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func (api *API) InitWorkTemplate() {
|
||||
api.BaseRoutes.WorkTemplates.Handle("/categories", api.APISessionRequired(getWorkTemplateCategories)).Methods("GET")
|
||||
api.BaseRoutes.WorkTemplates.Handle("/categories/{category}/templates", api.APISessionRequired(getWorkTemplates)).Methods("GET")
|
||||
api.BaseRoutes.WorkTemplates.Handle("/execute", api.APIHandler(executeWorkTemplate)).Methods("POST")
|
||||
}
|
||||
|
||||
func areWorkTemplatesEnabled(c *Context) *model.AppError {
|
||||
if !c.App.Config().FeatureFlags.WorkTemplate {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "feature flag is off", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// we have to make sure that playbooks plugin is enabled and board is a product
|
||||
pbActive, err := c.App.IsPluginActive(model.PluginIdPlaybooks)
|
||||
if err != nil {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if !pbActive {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "playbook plugin not active", http.StatusNotFound)
|
||||
}
|
||||
|
||||
hasBoard, err := c.App.HasBoardProduct()
|
||||
if err != nil {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if !hasBoard {
|
||||
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "board product not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWorkTemplateCategories(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
appErr := areWorkTemplatesEnabled(c)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
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, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func getWorkTemplates(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
appErr := areWorkTemplatesEnabled(c)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
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, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func executeWorkTemplate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
appErr := areWorkTemplatesEnabled(c)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
wtcr := &worktemplates.ExecutionRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(wtcr)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("executeWorkTemplate", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
canCreatePublicChannel := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionCreatePublicChannel)
|
||||
canCreatePrivateChannel := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionCreatePrivateChannel)
|
||||
// focalboard uses channel permissions for board creation
|
||||
canCreatePublicBoard := canCreatePublicChannel
|
||||
canCreatePrivateBoard := canCreatePrivateChannel
|
||||
canCreatePublicPlaybook := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionPublicPlaybookCreate)
|
||||
canCreatePrivatePlaybook := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionPrivatePlaybookCreate)
|
||||
appErr = wtcr.CanBeExecuted(worktemplates.PermissionSet{
|
||||
CanCreatePublicChannel: canCreatePublicChannel,
|
||||
CanCreatePrivateChannel: canCreatePrivateChannel,
|
||||
CanCreatePublicBoard: canCreatePublicBoard,
|
||||
CanCreatePrivateBoard: canCreatePrivateBoard,
|
||||
CanCreatePublicPlaybook: canCreatePublicPlaybook,
|
||||
CanCreatePrivatePlaybook: canCreatePrivatePlaybook,
|
||||
})
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
canInstallPlugin := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins)
|
||||
if !*c.App.Config().PluginSettings.Enable || !*c.App.Config().PluginSettings.EnableMarketplace || *c.App.Config().PluginSettings.MarketplaceURL != model.PluginSettingsDefaultMarketplaceURL {
|
||||
canInstallPlugin = false
|
||||
}
|
||||
|
||||
res, appErr := c.App.ExecuteWorkTemplate(c.AppContext, wtcr, canInstallPlugin)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(res)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("executeWorkTemplate", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/platform"
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
@@ -558,6 +559,7 @@ type AppIface interface {
|
||||
DownloadFromURL(downloadURL string) ([]byte, error)
|
||||
EnableUserAccessToken(token *model.UserAccessToken) *model.AppError
|
||||
EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any
|
||||
ExecuteWorkTemplate(c *request.Context, wtcr *worktemplates.ExecutionRequest, installPlugins bool) (*WorkTemplateExecutionResult, *model.AppError)
|
||||
ExportPermissions(w io.Writer) error
|
||||
ExtractContentFromFileInfo(fileInfo *model.FileInfo) error
|
||||
FetchSamlMetadataFromIdp(url string) ([]byte, *model.AppError)
|
||||
@@ -865,6 +867,7 @@ type AppIface interface {
|
||||
HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte)
|
||||
HandleIncomingWebhook(c *request.Context, hookID string, req *model.IncomingWebhookRequest) *model.AppError
|
||||
HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config)
|
||||
HasBoardProduct() (bool, error)
|
||||
HasPermissionTo(askingUserId string, permission *model.Permission) bool
|
||||
HasPermissionToChannel(c request.CTX, askingUserId string, channelID string, permission *model.Permission) bool
|
||||
HasPermissionToChannelByPost(askingUserId string, postID string, permission *model.Permission) bool
|
||||
@@ -891,6 +894,7 @@ type AppIface interface {
|
||||
IsLeader() bool
|
||||
IsPasswordValid(password string) *model.AppError
|
||||
IsPhase2MigrationCompleted() *model.AppError
|
||||
IsPluginActive(pluginName string) (bool, error)
|
||||
IsUserSignUpAllowed() *model.AppError
|
||||
JoinChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError
|
||||
JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError
|
||||
|
||||
95
app/mocks/WorkTemplateExecutor.go
Обычный файл
95
app/mocks/WorkTemplateExecutor.go
Обычный файл
@@ -0,0 +1,95 @@
|
||||
// Code generated by mockery v2.10.4. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make misc-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
request "github.com/mattermost/mattermost-server/v6/app/request"
|
||||
model "github.com/mattermost/mattermost-server/v6/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
worktemplates "github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
)
|
||||
|
||||
// WorkTemplateExecutor is an autogenerated mock type for the WorkTemplateExecutor type
|
||||
type WorkTemplateExecutor struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreateBoard provides a mock function with given fields: c, wtcr, cBoard, linkToChannelID
|
||||
func (_m *WorkTemplateExecutor) CreateBoard(c *request.Context, wtcr *worktemplates.ExecutionRequest, cBoard *model.WorkTemplateBoard, linkToChannelID string) (string, error) {
|
||||
ret := _m.Called(c, wtcr, cBoard, linkToChannelID)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateBoard, string) string); ok {
|
||||
r0 = rf(c, wtcr, cBoard, linkToChannelID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateBoard, string) error); ok {
|
||||
r1 = rf(c, wtcr, cBoard, linkToChannelID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateChannel provides a mock function with given fields: c, wtcr, cChannel
|
||||
func (_m *WorkTemplateExecutor) CreateChannel(c *request.Context, wtcr *worktemplates.ExecutionRequest, cChannel *model.WorkTemplateChannel) (string, error) {
|
||||
ret := _m.Called(c, wtcr, cChannel)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateChannel) string); ok {
|
||||
r0 = rf(c, wtcr, cChannel)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateChannel) error); ok {
|
||||
r1 = rf(c, wtcr, cChannel)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreatePlaybook provides a mock function with given fields: c, wtcr, playbook, channel
|
||||
func (_m *WorkTemplateExecutor) CreatePlaybook(c *request.Context, wtcr *worktemplates.ExecutionRequest, playbook *model.WorkTemplatePlaybook, channel model.WorkTemplateChannel) (string, error) {
|
||||
ret := _m.Called(c, wtcr, playbook, channel)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplatePlaybook, model.WorkTemplateChannel) string); ok {
|
||||
r0 = rf(c, wtcr, playbook, channel)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplatePlaybook, model.WorkTemplateChannel) error); ok {
|
||||
r1 = rf(c, wtcr, playbook, channel)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// InstallPlugin provides a mock function with given fields: c, wtcr, cIntegration, sendToChannelID
|
||||
func (_m *WorkTemplateExecutor) InstallPlugin(c *request.Context, wtcr *worktemplates.ExecutionRequest, cIntegration *model.WorkTemplateIntegration, sendToChannelID string) error {
|
||||
ret := _m.Called(c, wtcr, cIntegration, sendToChannelID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateIntegration, string) error); ok {
|
||||
r0 = rf(c, wtcr, cIntegration, sendToChannelID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/app/platform"
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/mattermost/mattermost-server/v6/audit"
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
@@ -4070,6 +4071,28 @@ func (a *OpenTracingAppLayer) ExecuteCommand(c request.CTX, args *model.CommandA
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) ExecuteWorkTemplate(c *request.Context, wtcr *worktemplates.ExecutionRequest, installPlugins bool) (*app.WorkTemplateExecutionResult, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExecuteWorkTemplate")
|
||||
|
||||
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.ExecuteWorkTemplate(c, wtcr, installPlugins)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) ExportPermissions(w io.Writer) error {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExportPermissions")
|
||||
@@ -11480,6 +11503,28 @@ func (a *OpenTracingAppLayer) HandleMessageExportConfig(cfg *model.Config, appCf
|
||||
a.app.HandleMessageExportConfig(cfg, appCfg)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) HasBoardProduct() (bool, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasBoardProduct")
|
||||
|
||||
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.HasBoardProduct()
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) HasPermissionTo(askingUserId string, permission *model.Permission) bool {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionTo")
|
||||
@@ -12025,6 +12070,28 @@ func (a *OpenTracingAppLayer) IsPhase2MigrationCompleted() *model.AppError {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) IsPluginActive(pluginName string) (bool, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPluginActive")
|
||||
|
||||
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.IsPluginActive(pluginName)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) IsUserSignUpAllowed() *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsUserSignUpAllowed")
|
||||
|
||||
@@ -1148,3 +1148,16 @@ func (ch *Channels) getPluginStateOverride(pluginID string) (bool, bool) {
|
||||
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (a *App) IsPluginActive(pluginName string) (bool, error) {
|
||||
return a.Channels().IsPluginActive(pluginName)
|
||||
}
|
||||
|
||||
func (ch *Channels) IsPluginActive(pluginName string) (bool, error) {
|
||||
pluginStatus, err := ch.GetPluginStatus(pluginName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return pluginStatus.State == model.PluginStateRunning, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -75,3 +76,21 @@ func (s *Server) shouldStart(product string) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) HasBoardProduct() (bool, error) {
|
||||
prod, exists := s.services[product.BoardsKey]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
if prod == nil {
|
||||
return false, errors.New("board product is nil")
|
||||
}
|
||||
if _, ok := prod.(product.BoardsService); !ok {
|
||||
return false, errors.New("board product key does not match its definition")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *App) HasBoardProduct() (bool, error) {
|
||||
return a.Srv().HasBoardProduct()
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ type Server struct {
|
||||
tracer *tracing.Tracer
|
||||
|
||||
products map[string]product.Product
|
||||
services map[product.ServiceKey]any
|
||||
|
||||
hooksManager *product.HooksManager
|
||||
}
|
||||
@@ -164,6 +165,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
LocalRouter: localRouter,
|
||||
timezones: timezones.New(),
|
||||
products: make(map[string]product.Product),
|
||||
services: make(map[product.ServiceKey]any),
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
@@ -262,6 +264,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to initialize products")
|
||||
}
|
||||
s.services = serviceMap
|
||||
|
||||
// After channel is initialized set it to the App object
|
||||
channelsWrapper, ok := serviceMap[product.ChannelKey].(*channelsWrapper)
|
||||
|
||||
278
app/work_template_executor.go
Обычный файл
278
app/work_template_executor.go
Обычный файл
@@ -0,0 +1,278 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
fb_model "github.com/mattermost/focalboard/server/model"
|
||||
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
type WorkTemplateExecutor interface {
|
||||
CreatePlaybook(c *request.Context, wtcr *worktemplates.ExecutionRequest, playbook *model.WorkTemplatePlaybook, channel model.WorkTemplateChannel) (string, error)
|
||||
CreateChannel(c *request.Context, wtcr *worktemplates.ExecutionRequest, cChannel *model.WorkTemplateChannel) (string, error)
|
||||
CreateBoard(c *request.Context, wtcr *worktemplates.ExecutionRequest, cBoard *model.WorkTemplateBoard, linkToChannelID string) (string, error)
|
||||
InstallPlugin(c *request.Context, wtcr *worktemplates.ExecutionRequest, cIntegration *model.WorkTemplateIntegration, sendToChannelID string) error
|
||||
}
|
||||
|
||||
type appWorkTemplateExecutor struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
func (e *appWorkTemplateExecutor) CreatePlaybook(
|
||||
c *request.Context,
|
||||
wtcr *worktemplates.ExecutionRequest,
|
||||
playbook *model.WorkTemplatePlaybook,
|
||||
channel model.WorkTemplateChannel) (string, error) {
|
||||
// determine playbook name
|
||||
name := playbook.Name
|
||||
if wtcr.Name != "" {
|
||||
name = fmt.Sprintf("%s: %s", wtcr.Name, playbook.Name)
|
||||
}
|
||||
|
||||
// get the correct playbook pbTemplate
|
||||
pbTemplate, err := wtcr.FindPlaybookTemplate(playbook.Template)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to find playbook template: %w", err)
|
||||
}
|
||||
|
||||
pbTemplate.TeamID = wtcr.TeamID
|
||||
pbTemplate.Title = name
|
||||
pbTemplate.Public = wtcr.Visibility == model.WorkTemplateVisibilityPublic
|
||||
data, err := json.Marshal(pbTemplate)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to marshal playbook template: %w", err)
|
||||
}
|
||||
|
||||
resp, appErr := e.app.doPluginRequest(c, http.MethodPost, "/plugins/playbooks/api/v0/playbooks", nil, data)
|
||||
if appErr != nil {
|
||||
return "", fmt.Errorf("unable to create playbook: %w", appErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
pbcResp := playbookCreateResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&pbcResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to decode playbook create response: %w", err)
|
||||
}
|
||||
|
||||
runName := channel.Name
|
||||
if wtcr.Name != "" {
|
||||
runName = fmt.Sprintf("%s: %s", wtcr.Name, channel.Name)
|
||||
}
|
||||
data, err = json.Marshal(pbclient.PlaybookRunCreateOptions{
|
||||
Name: runName,
|
||||
OwnerUserID: c.Session().UserId,
|
||||
TeamID: wtcr.TeamID,
|
||||
PlaybookID: pbcResp.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to marshal playbook run create request: %w", err)
|
||||
}
|
||||
resp, appErr = e.app.doPluginRequest(c, http.MethodPost, "/plugins/playbooks/api/v0/runs", nil, data)
|
||||
if appErr != nil {
|
||||
return "", fmt.Errorf("unable to create playbook run: %w", appErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
pbrResp := playbookRunCreateResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&pbrResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to decode playbook run create response: %w", err)
|
||||
}
|
||||
|
||||
// using pbrResp.ChannelID, update the channel to add metadata
|
||||
dbChannel, err := e.app.Srv().Store().Channel().Get(pbrResp.ChannelID, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to find channel: %w", err)
|
||||
}
|
||||
if dbChannel == nil {
|
||||
return "", fmt.Errorf("channel not found")
|
||||
}
|
||||
dbChannel.AddProp(model.WorkTemplateIDChannelProp, wtcr.WorkTemplate.ID)
|
||||
_, err = e.app.Srv().Store().Channel().Update(dbChannel)
|
||||
if err != nil {
|
||||
e.app.Srv().Log().Error("Failed to update playbook channel metadata", mlog.Err(err))
|
||||
}
|
||||
|
||||
return pbrResp.ChannelID, nil
|
||||
}
|
||||
|
||||
func (e *appWorkTemplateExecutor) CreateChannel(
|
||||
c *request.Context,
|
||||
wtcr *worktemplates.ExecutionRequest,
|
||||
cChannel *model.WorkTemplateChannel,
|
||||
) (string, error) {
|
||||
channelID := ""
|
||||
channelDisplayName := cChannel.Name
|
||||
if wtcr.Name != "" {
|
||||
channelDisplayName = fmt.Sprintf("%s: %s", wtcr.Name, cChannel.Name)
|
||||
}
|
||||
|
||||
var channelCreationAppErr *model.AppError = &model.AppError{}
|
||||
cleanChannelName := cleanChannelName(channelDisplayName)
|
||||
channelName := cleanChannelName
|
||||
if len(channelName) > model.ChannelNameMaxLength {
|
||||
channelName = channelName[:model.ChannelNameMaxLength]
|
||||
}
|
||||
|
||||
// Mostly because of the "quick use" feature, we might try to create channel that have the exact same "Name"
|
||||
// This loop ensures that if the original name is taken, we try again by adding a suffix to the Name
|
||||
for channelCreationAppErr != nil {
|
||||
// create channel
|
||||
var newChan *model.Channel
|
||||
newChan, channelCreationAppErr = e.app.CreateChannelWithUser(c, &model.Channel{
|
||||
TeamId: wtcr.TeamID,
|
||||
Name: channelName,
|
||||
DisplayName: channelDisplayName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
Purpose: cChannel.Purpose,
|
||||
Props: map[string]any{
|
||||
model.WorkTemplateIDChannelProp: wtcr.WorkTemplate.ID,
|
||||
},
|
||||
}, c.Session().UserId)
|
||||
if channelCreationAppErr != nil {
|
||||
if channelCreationAppErr.Id == store.ChannelExistsError {
|
||||
// compute a new unique name
|
||||
suffix := fmt.Sprintf("-%s", model.NewId()[0:4])
|
||||
channelName = cleanChannelName
|
||||
if len(cleanChannelName)+len(suffix) > model.ChannelNameMaxLength {
|
||||
channelName = cleanChannelName[:model.ChannelNameMaxLength-len(suffix)]
|
||||
}
|
||||
channelName = channelName + suffix
|
||||
continue
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("error while creating channel: %w", channelCreationAppErr)
|
||||
}
|
||||
channelID = newChan.Id
|
||||
}
|
||||
|
||||
return channelID, nil
|
||||
}
|
||||
|
||||
func (e *appWorkTemplateExecutor) CreateBoard(
|
||||
c *request.Context,
|
||||
wtcr *worktemplates.ExecutionRequest,
|
||||
cBoard *model.WorkTemplateBoard,
|
||||
linkToChannelID string,
|
||||
) (string, error) {
|
||||
boardService := e.app.Srv().services[product.BoardsKey].(product.BoardsService)
|
||||
templates, err := boardService.GetTemplates("0", c.Session().UserId)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error while getting templates: %w", err)
|
||||
}
|
||||
|
||||
var template *fb_model.Board = nil
|
||||
for _, t := range templates {
|
||||
v, ok := t.Properties["trackingTemplateId"]
|
||||
if ok && v == cBoard.Template {
|
||||
template = t
|
||||
break
|
||||
}
|
||||
}
|
||||
if template == nil {
|
||||
return "", fmt.Errorf("template not found")
|
||||
}
|
||||
|
||||
title := cBoard.Name
|
||||
if wtcr.Name != "" {
|
||||
title = fmt.Sprintf("%s: %s", wtcr.Name, cBoard.Name)
|
||||
}
|
||||
|
||||
// Duplicate board From template
|
||||
boardsAndBlocks, _, err := boardService.DuplicateBoard(template.ID, c.Session().UserId, wtcr.TeamID, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create new board from template: %w", err)
|
||||
}
|
||||
if len(boardsAndBlocks.Boards) != 1 {
|
||||
return "", fmt.Errorf("only one board was expected, found %d", len(boardsAndBlocks.Boards))
|
||||
}
|
||||
|
||||
// Apply patch for the title and linked channel
|
||||
patchedBoard, err := boardService.PatchBoard(&fb_model.BoardPatch{
|
||||
Title: &title,
|
||||
ChannelID: &linkToChannelID,
|
||||
}, boardsAndBlocks.Boards[0].ID, c.Session().UserId)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to patch board: %w", err)
|
||||
}
|
||||
|
||||
return patchedBoard.ID, nil
|
||||
}
|
||||
|
||||
func (e *appWorkTemplateExecutor) InstallPlugin(
|
||||
c *request.Context,
|
||||
wtcr *worktemplates.ExecutionRequest,
|
||||
cIntegration *model.WorkTemplateIntegration,
|
||||
sendToChannelID string,
|
||||
) error {
|
||||
// check if this plugin is already installed
|
||||
pluginID := cIntegration.ID
|
||||
_, appErr := e.app.GetPluginStatus(pluginID)
|
||||
if appErr != nil {
|
||||
if appErr.Id == "app.plugin.not_installed.app_error" {
|
||||
// we install them in the background as we don't want user to wait for this
|
||||
manifest, installAppErr := e.app.Channels().InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{
|
||||
Id: pluginID,
|
||||
Version: "",
|
||||
})
|
||||
if installAppErr != nil {
|
||||
return fmt.Errorf("unable to install plugin: %w", installAppErr)
|
||||
}
|
||||
if sendToChannelID != "" {
|
||||
e.app.SendEphemeralPost(c, c.Session().UserId, &model.Post{
|
||||
ChannelId: sendToChannelID,
|
||||
Message: fmt.Sprintf("plugin %s has been installed", manifest.Name),
|
||||
CreateAt: model.GetMillis(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unable to get plugin status: %w", appErr)
|
||||
}
|
||||
}
|
||||
|
||||
// get plugin state
|
||||
if err := e.app.EnablePlugin(pluginID); err != nil {
|
||||
return fmt.Errorf("unable to enable plugin: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type playbookCreateResponse struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type playbookRunCreateResponse struct {
|
||||
ID string `json:"id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
}
|
||||
|
||||
// cleaning channel name code bellow comes from the playbook repository.
|
||||
var allNonSpaceNonWordRegex = regexp.MustCompile(`[^\w\s]`)
|
||||
|
||||
func cleanChannelName(channelName string) string {
|
||||
// Lower case only
|
||||
channelName = strings.ToLower(channelName)
|
||||
// Trim spaces
|
||||
channelName = strings.TrimSpace(channelName)
|
||||
// Change all dashes to whitespace, remove everything that's not a word or whitespace, all space becomes dashes
|
||||
channelName = strings.ReplaceAll(channelName, "-", " ")
|
||||
channelName = allNonSpaceNonWordRegex.ReplaceAllString(channelName, "")
|
||||
channelName = strings.ReplaceAll(channelName, " ", "-")
|
||||
// Remove all leading and trailing dashes
|
||||
channelName = strings.Trim(channelName, "-")
|
||||
|
||||
return channelName
|
||||
}
|
||||
183
app/work_templates.go
Обычный файл
183
app/work_templates.go
Обычный файл
@@ -0,0 +1,183 @@
|
||||
// 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/request"
|
||||
"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, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
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, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (a *App) ExecuteWorkTemplate(c *request.Context, wtcr *worktemplates.ExecutionRequest, installPlugins bool) (*WorkTemplateExecutionResult, *model.AppError) {
|
||||
e := &appWorkTemplateExecutor{app: a}
|
||||
return a.executeWorkTemplate(c, wtcr, e, installPlugins)
|
||||
}
|
||||
|
||||
func (a *App) executeWorkTemplate(
|
||||
c *request.Context,
|
||||
wtcr *worktemplates.ExecutionRequest,
|
||||
e WorkTemplateExecutor,
|
||||
installPlugins bool,
|
||||
) (*WorkTemplateExecutionResult, *model.AppError) {
|
||||
res := &WorkTemplateExecutionResult{
|
||||
ChannelWithPlaybookIDs: []string{},
|
||||
ChannelIDs: []string{},
|
||||
}
|
||||
|
||||
contentByType := map[string][]model.WorkTemplateContent{
|
||||
"channel": {},
|
||||
"board": {},
|
||||
"playbook": {},
|
||||
"integration": {},
|
||||
}
|
||||
for _, content := range wtcr.WorkTemplate.Content {
|
||||
if content.Channel != nil {
|
||||
contentByType["channel"] = append(contentByType["channel"], content)
|
||||
}
|
||||
if content.Board != nil {
|
||||
contentByType["board"] = append(contentByType["board"], content)
|
||||
}
|
||||
if content.Playbook != nil {
|
||||
contentByType["playbook"] = append(contentByType["playbook"], content)
|
||||
}
|
||||
if content.Integration != nil {
|
||||
contentByType["integration"] = append(contentByType["integration"], content)
|
||||
}
|
||||
}
|
||||
|
||||
firstChannelId := ""
|
||||
channelIDByWorkTemplateID := map[string]string{}
|
||||
for _, pbContent := range contentByType["playbook"] {
|
||||
cPlaybook := pbContent.Playbook
|
||||
|
||||
// find associated channel
|
||||
var associatedChannel *model.WorkTemplateChannel
|
||||
for _, channelContent := range contentByType["channel"] {
|
||||
if channelContent.Channel.Playbook == cPlaybook.ID {
|
||||
associatedChannel = channelContent.Channel
|
||||
break
|
||||
}
|
||||
}
|
||||
if associatedChannel == nil {
|
||||
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.playbooks.find_channel_error", nil, "no associated channel found for playbook", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
channelID, err := e.CreatePlaybook(c, wtcr, cPlaybook, *associatedChannel)
|
||||
if err != nil {
|
||||
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.playbooks.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if firstChannelId == "" {
|
||||
firstChannelId = channelID
|
||||
}
|
||||
res.ChannelWithPlaybookIDs = append(res.ChannelWithPlaybookIDs, channelID)
|
||||
channelIDByWorkTemplateID[associatedChannel.ID] = channelID
|
||||
}
|
||||
|
||||
// loop through all channels
|
||||
for _, channelContent := range contentByType["channel"] {
|
||||
cChannel := channelContent.Channel
|
||||
// we only need to create a channel if there's no playbook
|
||||
if cChannel.Playbook == "" {
|
||||
chanID, err := e.CreateChannel(c, wtcr, cChannel)
|
||||
if err != nil {
|
||||
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.channels.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if firstChannelId == "" {
|
||||
firstChannelId = chanID
|
||||
}
|
||||
res.ChannelIDs = append(res.ChannelIDs, chanID)
|
||||
channelIDByWorkTemplateID[cChannel.ID] = chanID
|
||||
}
|
||||
}
|
||||
|
||||
for _, boardContent := range contentByType["board"] {
|
||||
cBoard := boardContent.Board
|
||||
channelID := ""
|
||||
if cBoard.Channel != "" {
|
||||
channel, ok := channelIDByWorkTemplateID[cBoard.Channel]
|
||||
if !ok {
|
||||
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.app_error", nil, "no associated channel found for board", http.StatusInternalServerError)
|
||||
}
|
||||
channelID = channel
|
||||
}
|
||||
|
||||
_, err := e.CreateBoard(c, wtcr, cBoard, channelID)
|
||||
if err != nil {
|
||||
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.boards.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if installPlugins {
|
||||
for _, integrationContent := range contentByType["integration"] {
|
||||
cIntegration := integrationContent.Integration
|
||||
// this can take a long time so we just start those as background tasks
|
||||
go e.InstallPlugin(c, wtcr, cIntegration, firstChannelId)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ch := range res.ChannelWithPlaybookIDs {
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", c.Session().UserId, nil, "")
|
||||
message.Add("channel_id", ch)
|
||||
message.Add("team_id", wtcr.TeamID)
|
||||
a.Publish(message)
|
||||
}
|
||||
for _, ch := range res.ChannelIDs {
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", c.Session().UserId, nil, "")
|
||||
message.Add("channel_id", ch)
|
||||
message.Add("team_id", wtcr.TeamID)
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type WorkTemplateExecutionResult struct {
|
||||
ChannelWithPlaybookIDs []string `json:"channel_with_playbook_ids"`
|
||||
ChannelIDs []string `json:"channel_ids"`
|
||||
}
|
||||
231
app/work_templates_test.go
Обычный файл
231
app/work_templates_test.go
Обычный файл
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
|
||||
)
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteWorkTemplate(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
c := request.EmptyContext(th.App.Log())
|
||||
c.SetSession(&model.Session{UserId: model.NewId()})
|
||||
|
||||
req := &worktemplates.ExecutionRequest{
|
||||
TeamID: "team-1",
|
||||
Name: "test",
|
||||
WorkTemplate: model.WorkTemplate{
|
||||
ID: "test-template",
|
||||
Content: []model.WorkTemplateContent{
|
||||
{
|
||||
Playbook: &model.WorkTemplatePlaybook{
|
||||
Name: "test playbook",
|
||||
Template: "test template pb",
|
||||
ID: "test-playbook",
|
||||
},
|
||||
},
|
||||
{
|
||||
Channel: &model.WorkTemplateChannel{
|
||||
// this will not create a channel directly
|
||||
// playbooks will do it
|
||||
Name: "test channel",
|
||||
Playbook: "test-playbook",
|
||||
},
|
||||
},
|
||||
{
|
||||
Channel: &model.WorkTemplateChannel{
|
||||
Name: "test channel 2 that will be created",
|
||||
ID: "channel-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &model.WorkTemplateBoard{
|
||||
Name: "test board",
|
||||
Template: "test template board",
|
||||
Channel: "channel-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &model.WorkTemplateBoard{
|
||||
Name: "test board with no channel linked",
|
||||
Template: "test template board",
|
||||
},
|
||||
},
|
||||
{
|
||||
Integration: &model.WorkTemplateIntegration{
|
||||
ID: "test-plugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PlaybookTemplates: []*worktemplates.PlaybookTemplate{
|
||||
{
|
||||
Title: "test template pb",
|
||||
Template: pbclient.PlaybookCreateOptions{
|
||||
Title: "test playbook",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run("with install plugin enabled", func(t *testing.T) {
|
||||
executorMock := &mocks.WorkTemplateExecutor{}
|
||||
executorMock.On("CreatePlaybook", c, req, req.WorkTemplate.Content[0].Playbook, *req.WorkTemplate.Content[1].Channel).Return("channel-1", nil)
|
||||
executorMock.On("CreateChannel", c, req, req.WorkTemplate.Content[2].Channel).Return("channel-2", nil)
|
||||
executorMock.On("CreateBoard", c, req, req.WorkTemplate.Content[3].Board, "channel-2").Return("", nil)
|
||||
executorMock.On("CreateBoard", c, req, req.WorkTemplate.Content[4].Board, "").Return("", nil)
|
||||
executorMock.On("InstallPlugin", c, req, req.WorkTemplate.Content[5].Integration, "channel-1").Return(nil)
|
||||
|
||||
res, appErr := th.App.executeWorkTemplate(c, req, executorMock, true)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, "channel-1", res.ChannelWithPlaybookIDs[0])
|
||||
assert.Equal(t, "channel-2", res.ChannelIDs[0])
|
||||
// give some time as plugin are called in a gorouting
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
executorMock.AssertExpectations(t)
|
||||
})
|
||||
t.Run("with install plugin disabled", func(t *testing.T) {
|
||||
executorMock := &mocks.WorkTemplateExecutor{}
|
||||
executorMock.On("CreatePlaybook", c, req, req.WorkTemplate.Content[0].Playbook, *req.WorkTemplate.Content[1].Channel).Return("channel-1", nil)
|
||||
executorMock.On("CreateChannel", c, req, req.WorkTemplate.Content[2].Channel).Return("channel-2", nil)
|
||||
executorMock.On("CreateBoard", c, req, req.WorkTemplate.Content[3].Board, "channel-2").Return("", nil)
|
||||
executorMock.On("CreateBoard", c, req, req.WorkTemplate.Content[4].Board, "").Return("", nil)
|
||||
// the lack of call to InstallPlugin is the difference with the previous test
|
||||
|
||||
res, appErr := th.App.executeWorkTemplate(c, req, executorMock, false)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, "channel-1", res.ChannelWithPlaybookIDs[0])
|
||||
assert.Equal(t, "channel-2", res.ChannelIDs[0])
|
||||
// give some time as plugin are called in a gorouting
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
executorMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
112
app/worktemplates/model.go
Обычный файл
112
app/worktemplates/model.go
Обычный файл
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
package worktemplates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
type ExecutionRequest struct {
|
||||
TeamID string `json:"team_id"`
|
||||
Name string `json:"name"`
|
||||
Visibility string `json:"visibility"`
|
||||
WorkTemplate model.WorkTemplate `json:"work_template"`
|
||||
PlaybookTemplates []*PlaybookTemplate `json:"playbook_templates"`
|
||||
|
||||
foundPlaybookTemplates map[string]*pbclient.PlaybookCreateOptions
|
||||
}
|
||||
|
||||
type PermissionSet struct {
|
||||
// channels
|
||||
CanCreatePublicChannel bool
|
||||
CanCreatePrivateChannel bool
|
||||
// playbooks
|
||||
CanCreatePublicPlaybook bool
|
||||
CanCreatePrivatePlaybook bool
|
||||
// boards
|
||||
CanCreatePublicBoard bool
|
||||
CanCreatePrivateBoard bool
|
||||
}
|
||||
|
||||
func (r *ExecutionRequest) CanBeExecuted(p PermissionSet) *model.AppError {
|
||||
public := r.Visibility == model.WorkTemplateVisibilityPublic
|
||||
for _, c := range r.WorkTemplate.Content {
|
||||
if c.Channel != nil {
|
||||
if public && !p.CanCreatePublicChannel {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_channel", nil, "", http.StatusForbidden)
|
||||
}
|
||||
if !public && !p.CanCreatePrivateChannel {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_channel", nil, "", http.StatusForbidden)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if c.Board != nil {
|
||||
if public && !p.CanCreatePublicBoard {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_board", nil, "", http.StatusForbidden)
|
||||
}
|
||||
if !public && !p.CanCreatePrivateBoard {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_board", nil, "", http.StatusForbidden)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if c.Playbook != nil {
|
||||
if public && !p.CanCreatePublicPlaybook {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_playbook", nil, "", http.StatusForbidden)
|
||||
}
|
||||
if !public && !p.CanCreatePrivatePlaybook {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_playbook", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
// we need to check what's the template default run execution mode
|
||||
// to determine how the channel is created
|
||||
tmpl, err := r.FindPlaybookTemplate(c.Playbook.Template)
|
||||
if err != nil {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_find_playbook_template", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if tmpl.CreatePublicPlaybookRun && !p.CanCreatePublicChannel {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_run", nil, "", http.StatusForbidden)
|
||||
}
|
||||
if !tmpl.CreatePublicPlaybookRun && !p.CanCreatePrivateChannel {
|
||||
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_run", nil, "", http.StatusForbidden)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindPlaybookTemplate returns the playbook template with the given title.
|
||||
// it also feed a cache to avoid looking for the same template twice.
|
||||
func (r *ExecutionRequest) FindPlaybookTemplate(templateTitle string) (*pbclient.PlaybookCreateOptions, error) {
|
||||
if r.foundPlaybookTemplates == nil {
|
||||
r.foundPlaybookTemplates = make(map[string]*pbclient.PlaybookCreateOptions)
|
||||
}
|
||||
|
||||
if pt, ok := r.foundPlaybookTemplates[templateTitle]; ok {
|
||||
if pt == nil {
|
||||
return nil, errors.New("playbook template not found")
|
||||
}
|
||||
return pt, nil
|
||||
}
|
||||
|
||||
for _, pt := range r.PlaybookTemplates {
|
||||
if pt.Title == templateTitle {
|
||||
r.foundPlaybookTemplates[templateTitle] = &pt.Template
|
||||
return &pt.Template, nil
|
||||
}
|
||||
}
|
||||
r.foundPlaybookTemplates[templateTitle] = nil
|
||||
return nil, errors.New("playbook template not found")
|
||||
}
|
||||
|
||||
type PlaybookTemplate struct {
|
||||
Title string `json:"title"`
|
||||
Template pbclient.PlaybookCreateOptions `json:"template"`
|
||||
}
|
||||
81
app/worktemplates/model_test.go
Обычный файл
81
app/worktemplates/model_test.go
Обычный файл
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
package worktemplates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
|
||||
)
|
||||
|
||||
func TestCanBeExecuted(t *testing.T) {
|
||||
wtcr := &ExecutionRequest{
|
||||
Visibility: model.WorkTemplateVisibilityPublic,
|
||||
WorkTemplate: model.WorkTemplate{
|
||||
Content: []model.WorkTemplateContent{
|
||||
{
|
||||
Playbook: &model.WorkTemplatePlaybook{
|
||||
Name: "test playbook",
|
||||
ID: "test-pb",
|
||||
Template: "test template pb",
|
||||
},
|
||||
},
|
||||
{
|
||||
Channel: &model.WorkTemplateChannel{
|
||||
ID: "test-channel",
|
||||
Name: "test channel",
|
||||
Playbook: "test-pb",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &model.WorkTemplateBoard{
|
||||
Name: "test board",
|
||||
Channel: "test-channel",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PlaybookTemplates: []*PlaybookTemplate{
|
||||
{
|
||||
Title: "test template pb",
|
||||
Template: pbclient.PlaybookCreateOptions{
|
||||
CreatePublicPlaybookRun: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("can run when all permissions are good", func(t *testing.T) {
|
||||
appErr := wtcr.CanBeExecuted(PermissionSet{
|
||||
CanCreatePublicChannel: true,
|
||||
CanCreatePublicPlaybook: true,
|
||||
CanCreatePublicBoard: true,
|
||||
})
|
||||
assert.Nil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("fails when something is not allowed", func(t *testing.T) {
|
||||
appErr := wtcr.CanBeExecuted(PermissionSet{
|
||||
CanCreatePublicChannel: true,
|
||||
CanCreatePublicPlaybook: false,
|
||||
CanCreatePublicBoard: true,
|
||||
})
|
||||
require.NotNil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("returns an error and no res when playbook template is not found", func(t *testing.T) {
|
||||
wtcrMod := *wtcr
|
||||
wtcrMod.foundPlaybookTemplates = map[string]*pbclient.PlaybookCreateOptions{}
|
||||
wtcrMod.PlaybookTemplates = []*PlaybookTemplate{}
|
||||
appErr := wtcrMod.CanBeExecuted(PermissionSet{
|
||||
CanCreatePublicChannel: true,
|
||||
CanCreatePublicPlaybook: true,
|
||||
CanCreatePublicBoard: true,
|
||||
})
|
||||
require.NotNil(t, appErr)
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
id: "product_teams/feature_release:v1"
|
||||
category: product_teams
|
||||
useCase: Feature Release
|
||||
illustration: https://via.placeholder.com/204x123.png
|
||||
illustration: /static/worktemplates/product_teams/feature_release/feature_release.png
|
||||
visibility: public
|
||||
description:
|
||||
channel:
|
||||
@@ -16,30 +16,30 @@ description:
|
||||
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"
|
||||
illustration: "/static/worktemplates/product_teams/feature_release/integrations.png"
|
||||
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"
|
||||
playbook: product-release-playbook
|
||||
illustration: "/static/worktemplates/product_teams/feature_release/channel.png"
|
||||
- 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?
|
||||
template: "54fcf9c610f0ac5e4c522c0657c90602"
|
||||
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"
|
||||
channel: feature-release
|
||||
illustration: "/static/worktemplates/product_teams/feature_release/board-ma.png"
|
||||
- board:
|
||||
id: "board-project-task"
|
||||
template: "project task|bmttiziw35irgtmztewd9upyqdy"
|
||||
name: project task board
|
||||
template: "a4ec399ab4f2088b1051c3cdf1dde4c3"
|
||||
name: Project Task
|
||||
channel: feature-release
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Board+project+task"
|
||||
illustration: "/static/worktemplates/product_teams/feature_release/board-pt.png"
|
||||
- playbook:
|
||||
template: "product release" # <-- playbooks templates don't have ids, have to rely on name
|
||||
template: "Product Release"
|
||||
name: "Feature release"
|
||||
id: product-release-playbook
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Playbook+feature+release"
|
||||
illustration: "/static/worktemplates/product_teams/feature_release/playbook.png"
|
||||
- integration:
|
||||
id: jira
|
||||
- integration:
|
||||
|
||||
@@ -228,8 +228,8 @@ func (wt WorkTemplate) Validate(categoryIds map[string]struct{}) error {
|
||||
}
|
||||
|
||||
type FeatureFlag struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Name string `yaml:"name"`
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
type TranslatableString struct {
|
||||
|
||||
@@ -29,7 +29,7 @@ var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{
|
||||
ID: "product_teams/feature_release:v1",
|
||||
Category: "product_teams",
|
||||
UseCase: "Feature Release",
|
||||
Illustration: "https://via.placeholder.com/204x123.png",
|
||||
Illustration: "/static/worktemplates/product_teams/feature_release/feature_release.png",
|
||||
Visibility: "public",
|
||||
|
||||
Description: Description{
|
||||
@@ -51,7 +51,7 @@ var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{
|
||||
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",
|
||||
Illustration: "/static/worktemplates/product_teams/feature_release/integrations.png",
|
||||
},
|
||||
},
|
||||
Content: []Content{
|
||||
@@ -61,33 +61,33 @@ var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{
|
||||
Name: "Feature Release",
|
||||
Purpose: "",
|
||||
Playbook: "product-release-playbook",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Channel+feature+release",
|
||||
Illustration: "/static/worktemplates/product_teams/feature_release/channel.png",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &Board{
|
||||
ID: "board-meeting-agenda",
|
||||
Template: "meeting agenda|bwps66irhr7b9dxgayf9kz33g5o",
|
||||
Template: "54fcf9c610f0ac5e4c522c0657c90602",
|
||||
Name: "Meeting Agenda",
|
||||
Channel: "feature-release",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Board+meeting+agenda",
|
||||
Illustration: "/static/worktemplates/product_teams/feature_release/board-ma.png",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &Board{
|
||||
ID: "board-project-task",
|
||||
Template: "project task|bmttiziw35irgtmztewd9upyqdy",
|
||||
Name: "project task board",
|
||||
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
|
||||
Name: "Project Task",
|
||||
Channel: "feature-release",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Board+project+task",
|
||||
Illustration: "/static/worktemplates/product_teams/feature_release/board-pt.png",
|
||||
},
|
||||
},
|
||||
{
|
||||
Playbook: &Playbook{
|
||||
Template: "product release",
|
||||
Template: "Product Release",
|
||||
Name: "Feature release",
|
||||
ID: "product-release-playbook",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Playbook+feature+release",
|
||||
Illustration: "/static/worktemplates/product_teams/feature_release/playbook.png",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// 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",
|
||||
},
|
||||
}
|
||||
}
|
||||
6
go.mod
6
go.mod
@@ -38,6 +38,7 @@ require (
|
||||
github.com/mattermost/gziphandler v0.0.1
|
||||
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d
|
||||
github.com/mattermost/logr/v2 v2.0.15
|
||||
github.com/mattermost/mattermost-plugin-playbooks/client v0.7.0
|
||||
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e
|
||||
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0
|
||||
github.com/mattermost/squirrel v0.2.0
|
||||
@@ -75,7 +76,6 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/HdrHistogram/hdrhistogram-go v0.9.0 // indirect
|
||||
github.com/JalfResi/justext v0.0.0-20221106200834-be571e3e3052 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.2.1 // indirect
|
||||
@@ -116,7 +116,7 @@ require (
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
@@ -174,7 +174,9 @@ require (
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf // indirect
|
||||
google.golang.org/grpc v1.51.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
|
||||
374
go.sum
374
go.sum
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
60
i18n/en.json
60
i18n/en.json
@@ -4519,6 +4519,10 @@
|
||||
"id": "api.websocket_handler.server_busy.app_error",
|
||||
"translation": "Server is busy, non-critical services are temporarily unavailable."
|
||||
},
|
||||
{
|
||||
"id": "api.work_templates.disabled",
|
||||
"translation": "Work templates are disabled."
|
||||
},
|
||||
{
|
||||
"id": "app.acknowledgement.delete.app_error",
|
||||
"translation": "Unable to delete acknowledgement."
|
||||
@@ -7035,6 +7039,62 @@
|
||||
"id": "app.webhooks.update_outgoing.app_error",
|
||||
"translation": "Unable to update the webhook."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_create_private_board",
|
||||
"translation": "You don't have permissions to create a private board."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_create_private_channel",
|
||||
"translation": "You don't have permissions to create a private channel."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_create_private_playbook",
|
||||
"translation": "You don't have permissions to create a private playbook."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_create_private_run",
|
||||
"translation": "You don't have permissions to create a private channel for the playbook run."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_create_public_board",
|
||||
"translation": "You don't have permissions to create a public board."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_create_public_channel",
|
||||
"translation": "You don't have permissions to create a public channel."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_create_public_playbook",
|
||||
"translation": "You don't have permissions to create a public playbook."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_create_public_run",
|
||||
"translation": "You don't have permissions to create a public channel for the playbook run."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplate.execution_request.cannot_find_playbook_template",
|
||||
"translation": "Unable to find playbook template associated with this work template."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplates.execute_work_template.app_error",
|
||||
"translation": "Error while executing a work template."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplates.execute_work_template.boards.create_error",
|
||||
"translation": "Error while creating a board."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplates.execute_work_template.channels.create_error",
|
||||
"translation": "Error while creating a channel."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplates.execute_work_template.playbooks.create_error",
|
||||
"translation": "Error while creating a playbook."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplates.execute_work_template.playbooks.find_channel_error",
|
||||
"translation": "Unable to find channel associated with a playbook."
|
||||
},
|
||||
{
|
||||
"id": "app.worktemplates.get_categories.app_error",
|
||||
"translation": "Unable to get work template categories"
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
|
||||
package model
|
||||
|
||||
const (
|
||||
// used to assign the work template id to newly created channels
|
||||
WorkTemplateIDChannelProp = "work_template_id"
|
||||
|
||||
// Visibility
|
||||
WorkTemplateVisibilityPublic = "public"
|
||||
WorkTemplateVisibilityPrivate = "private"
|
||||
)
|
||||
|
||||
type WorkTemplateCategory struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -69,3 +78,8 @@ type WorkTemplateContent struct {
|
||||
Playbook *WorkTemplatePlaybook `json:"playbook,omitempty"`
|
||||
Integration *WorkTemplateIntegration `json:"integration,omitempty"`
|
||||
}
|
||||
|
||||
type WorkTemplateExecutionResult struct {
|
||||
ChannelWithPlaybookIDs []string `json:"channel_with_playbook_ids"`
|
||||
ChannelIDs []string `json:"channel_ids"`
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user