[MM-47846] Execute Work Template (#22055)
Этот коммит содержится в:
@@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user