[MM-52926] Deprecating work templates (#23466)

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Julien Tant
2023-06-05 18:42:03 -07:00
коммит произвёл GitHub
родитель 22275fa0f5
Коммит 0468e772a9
184 изменённых файлов: 4 добавлений и 10688 удалений

Просмотреть файл

@@ -140,8 +140,6 @@ type Routes struct {
Usage *mux.Router // 'api/v4/usage'
WorkTemplates *mux.Router // 'api/v4/worktemplates'
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
Drafts *mux.Router // 'api/v4/drafts'
@@ -271,8 +269,6 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
api.BaseRoutes.WorkTemplates = api.BaseRoutes.APIRoot.PathPrefix("/worktemplates").Subrouter()
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
@@ -320,7 +316,6 @@ func Init(srv *app.Server) (*API, error) {
api.InitExport()
api.InitInsights()
api.InitUsage()
api.InitWorkTemplate()
api.InitHostedCustomer()
api.InitDrafts()
if err := api.InitGraphQL(); err != nil {

Просмотреть файл

@@ -1,139 +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/server/public/model"
"github.com/mattermost/mattermost-server/server/v8/channels/app/worktemplates"
)
const WorkTemplateContextOnboarding = "onboarding"
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)
}
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()
context := r.URL.Query().Get("context")
isOnboarding := false
if context == WorkTemplateContextOnboarding {
isOnboarding = true
}
workTemplates, appErr := c.App.GetWorkTemplates(c.Params.Category, c.App.Config().FeatureFlags.ToMap(), isOnboarding, 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{
License: c.App.License(),
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
}
}

Просмотреть файл

@@ -25,7 +25,6 @@ import (
"github.com/mattermost/mattermost-server/server/public/shared/timezones"
"github.com/mattermost/mattermost-server/server/v8/channels/app/platform"
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
"github.com/mattermost/mattermost-server/server/v8/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/server/v8/channels/audit"
"github.com/mattermost/mattermost-server/server/v8/channels/product"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
@@ -566,7 +565,6 @@ 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,8 +863,6 @@ type AppIface interface {
GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError)
GetWarnMetricsBot() (*model.Bot, *model.AppError)
GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError)
GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError)
GetWorkTemplates(category string, featureFlags map[string]string, includeOnboardingTemplates bool, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError)
HTTPService() httpservice.HTTPService
Handle404(w http.ResponseWriter, r *http.Request)
HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError)

Просмотреть файл

@@ -1,119 +0,0 @@
// Code generated by mockery v2.23.2. DO NOT EDIT.
// Regenerate this file using `make misc-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/server/public/model"
request "github.com/mattermost/mattermost-server/server/v8/channels/app/request"
mock "github.com/stretchr/testify/mock"
worktemplates "github.com/mattermost/mattermost-server/server/v8/channels/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
var r1 error
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateBoard, string) (string, error)); ok {
return rf(c, wtcr, cBoard, linkToChannelID)
}
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)
}
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
var r1 error
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateChannel) (string, error)); ok {
return rf(c, wtcr, cChannel)
}
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)
}
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
var r1 error
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplatePlaybook, model.WorkTemplateChannel) (string, error)); ok {
return rf(c, wtcr, playbook, channel)
}
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)
}
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
}
type mockConstructorTestingTNewWorkTemplateExecutor interface {
mock.TestingT
Cleanup(func())
}
// NewWorkTemplateExecutor creates a new instance of WorkTemplateExecutor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewWorkTemplateExecutor(t mockConstructorTestingTNewWorkTemplateExecutor) *WorkTemplateExecutor {
mock := &WorkTemplateExecutor{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

Просмотреть файл

@@ -45,16 +45,6 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo
}
}
if request.Role != "" {
err := a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.SystemFirstAdminRole,
Value: request.Role,
})
if err != nil {
a.Log().Error("failed to save first admin role", mlog.Err(err))
}
}
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
if pluginsEnvironment == nil {
return a.markAdminOnboardingComplete(c)

Просмотреть файл

@@ -28,21 +28,3 @@ func TestOnboardingSavesOrganizationName(t *testing.T) {
require.NoError(t, storeErr)
require.Equal(t, "Mattermost In Tests", sys.Value)
}
func TestOnboardingFirstAdminRole(t *testing.T) {
th := Setup(t)
defer th.TearDown()
err := th.App.CompleteOnboarding(&request.Context{}, &mm_model.CompleteOnboardingRequest{
Organization: "myorg",
Role: "engineering",
})
require.Nil(t, err)
defer func() {
th.App.Srv().Store().System().PermanentDeleteByName(mm_model.SystemFirstAdminRole)
}()
sys, storeErr := th.App.Srv().Store().System().GetByName(mm_model.SystemFirstAdminRole)
require.NoError(t, storeErr)
require.Equal(t, "engineering", sys.Value)
}

Просмотреть файл

@@ -26,7 +26,6 @@ import (
"github.com/mattermost/mattermost-server/server/v8/channels/app"
"github.com/mattermost/mattermost-server/server/v8/channels/app/platform"
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
"github.com/mattermost/mattermost-server/server/v8/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/server/v8/channels/audit"
"github.com/mattermost/mattermost-server/server/v8/channels/product"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
@@ -4105,28 +4104,6 @@ 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")
@@ -11382,50 +11359,6 @@ func (a *OpenTracingAppLayer) GetWarnMetricsStatus() (map[string]*model.WarnMetr
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplateCategories")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWorkTemplateCategories(t)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWorkTemplates(category string, featureFlags map[string]string, includeOnboardingTemplates bool, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplates")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWorkTemplates(category, featureFlags, includeOnboardingTemplates, t)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) Handle404(w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")

Просмотреть файл

@@ -1,53 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/public/shared/i18n"
"github.com/mattermost/mattermost-server/server/v8/channels/app"
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
)
type TemplatesProvider struct {
}
const (
CmdTemplates = "templates"
)
func init() {
app.RegisterCommandProvider(&TemplatesProvider{})
}
func (h *TemplatesProvider) GetTrigger() string {
return CmdTemplates
}
func (h *TemplatesProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
workTemplateEnabled := a.Config().FeatureFlags.WorkTemplate
pbActive, err := a.IsPluginActive(model.PluginIdPlaybooks)
if err != nil {
pbActive = false
}
hasBoard, err := a.HasBoardProduct()
if err != nil {
hasBoard = false
}
return &model.Command{
Trigger: CmdTemplates,
AutoComplete: hasBoard && pbActive && workTemplateEnabled,
AutoCompleteDesc: T("api.command_templates.desc"),
DisplayName: T("api.command_templates.name"),
}
}
func (h *TemplatesProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_templates.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}

Просмотреть файл

@@ -1,302 +0,0 @@
// 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"
"github.com/mattermost/mattermost-server/server/public/plugin"
pbclient "github.com/mattermost/mattermost-server/server/v8/playbooks/client"
fb_model "github.com/mattermost/mattermost-server/server/v8/boards/model"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
"github.com/mattermost/mattermost-server/server/v8/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/server/v8/channels/product"
"github.com/mattermost/mattermost-server/server/v8/channels/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 += " " + wtcr.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
pbTemplate.CreatePublicPlaybookRun = 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 = wtcr.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 = wtcr.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 += " " + wtcr.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)
}
hooks, err := e.app.ch.HooksForPluginOrProduct(pluginID)
if err != nil {
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err))
return nil
}
event := model.OnInstallEvent{
UserId: c.Session().UserId,
}
if err = hooks.OnInstall(&plugin.Context{
RequestId: c.RequestId(),
SessionId: c.Session().Id,
IPAddress: c.IPAddress(),
AcceptLanguage: c.AcceptLanguage(),
UserAgent: c.UserAgent(),
}, event); err != nil {
mlog.Error("Plugin OnInstall hook failed", mlog.String("plugin_id", pluginID), mlog.Err(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
}

Просмотреть файл

@@ -1,189 +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/server/public/model"
"github.com/mattermost/mattermost-server/server/public/shared/i18n"
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
"github.com/mattermost/mattermost-server/server/v8/channels/app/worktemplates"
)
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, includeOnboardingTemplates bool, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
templates, err := worktemplates.ListByCategory(category, includeOnboardingTemplates)
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{},
}
if wtcr.Name != "" {
if len(wtcr.Name) > model.ChannelNameMaxLength {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.name_too_long", nil, "", http.StatusBadRequest)
}
}
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"`
}

Просмотреть файл

@@ -1,257 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/v8/channels/app/mocks"
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
"github.com/mattermost/mattermost-server/server/v8/channels/app/worktemplates"
pbclient "github.com/mattermost/mattermost-server/server/v8/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",
},
},
},
{
ID: "test-template-4",
Category: firstCat.ID,
UseCase: "test use case 4",
OnboardingOnly: true,
},
{ // this one should not be returned because of the category
ID: "test-template-4",
Category: "cat-test2",
UseCase: "test use case 4",
},
}
t.Run("not onboarding", func(t *testing.T) {
// Act
worktemplates, appErr := th.App.GetWorkTemplates(firstCat.ID, ff, false, 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)
})
t.Run("onboarding", func(t *testing.T) {
// Act
worktemplates, appErr := th.App.GetWorkTemplates(firstCat.ID, ff, true, wtTranslationFunc)
// Assert
assert.Nil(appErr)
assert.Len(worktemplates, 3)
assert.Equal("test-template-4", worktemplates[2].ID)
})
}
// 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)
})
t.Run("with name too long", func(t *testing.T) {
req.Name = strings.Repeat("a", model.ChannelNameMaxLength+1)
_, appErr := th.App.executeWorkTemplate(c, req, nil, false)
assert.NotNil(t, appErr)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
}

Просмотреть файл

@@ -1,18 +0,0 @@
- id: product_teams
name: worktemplate.category.product_teams
- id: devops
name: worktemplate.category.devops
- id: leadership
name: worktemplate.category.leadership
- id: engineering
name: worktemplate.category.engineering
- id: project_management
name: worktemplate.category.project_management
- id: marketing
name: worktemplate.category.marketing
- id: design
name: worktemplate.category.design
- id: qa
name: worktemplate.category.qa
- id: other
name: worktemplate.category.other

Просмотреть файл

@@ -1,207 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"bytes"
"crypto/md5"
_ "embed"
"fmt"
"io"
"log"
"os"
"path"
"sort"
"text/template"
"github.com/pkg/errors"
"golang.org/x/tools/imports"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost-server/server/v8/channels/app/worktemplates"
)
type WorkTemplateWithMD5 struct {
worktemplates.WorkTemplate
MD5 string
}
type WorkTemplateCategoryWithMD5 struct {
worktemplates.WorkTemplateCategory `yaml:",inline"`
MD5 string
}
func getFileContent(filename string) ([]byte, error) {
return os.ReadFile(path.Join(filename))
}
func main() {
// parse categories first
dat, err := getFileContent("categories.yaml")
if err != nil {
log.Fatal(errors.Wrap(err, "failed to read categories.yaml"))
}
illustrations := []string{}
h := md5.New()
cats := []WorkTemplateCategoryWithMD5{} // meow
err = yaml.Unmarshal(dat, &cats)
if err != nil {
log.Fatal(errors.Wrap(err, "failed to unmarshal categories.yaml"))
}
// validate categories
categoryIds := map[string]struct{}{}
lastCategory := ""
for id := range cats {
cat := cats[id]
if cat.ID == "" && cat.Name == "" {
// skip empty array element
continue
}
if cat.ID == "" {
log.Fatal(errors.New("category ID cannot be empty"))
}
if cat.Name == "" {
log.Fatal(errors.New("category name cannot be empty"))
}
lastCategory = cat.ID
categoryIds[cat.ID] = struct{}{}
h.Write([]byte(cat.ID))
cats[id].MD5 = fmt.Sprintf("%x", h.Sum(nil))
h.Reset()
}
if lastCategory != "other" {
log.Fatal(errors.New("category 'other' must exist AND be the last category"))
}
dat, err = getFileContent("templates.yaml")
if err != nil {
log.Fatal(errors.Wrap(err, "failed to read templates.yaml"))
}
dec := yaml.NewDecoder(bytes.NewReader(dat))
ts := []WorkTemplateWithMD5{}
for {
t := worktemplates.WorkTemplate{}
err = dec.Decode(&t)
if err != nil {
if err == io.EOF {
break
}
log.Fatal(err)
}
if t.ID == "" {
continue
}
h.Write([]byte(t.ID))
err = t.Validate(categoryIds)
if err != nil {
log.Fatal(errors.Wrap(err, "failed to validate template"))
}
ts = append(ts, WorkTemplateWithMD5{
WorkTemplate: t,
MD5: fmt.Sprintf("%x", h.Sum(nil)),
})
h.Reset()
// add illustrations to the list
illustrations = append(illustrations, t.Illustration)
if t.Description.Channel != nil && t.Description.Channel.Illustration != "" {
illustrations = append(illustrations, t.Description.Channel.Illustration)
}
if t.Description.Board != nil && t.Description.Board.Illustration != "" {
illustrations = append(illustrations, t.Description.Board.Illustration)
}
if t.Description.Integration != nil && t.Description.Integration.Illustration != "" {
illustrations = append(illustrations, t.Description.Integration.Illustration)
}
if t.Description.Playbook != nil && t.Description.Playbook.Illustration != "" {
illustrations = append(illustrations, t.Description.Playbook.Illustration)
}
for i := range t.Content {
if t.Content[i].Channel != nil && t.Content[i].Channel.Illustration != "" {
illustrations = append(illustrations, t.Content[i].Channel.Illustration)
}
if t.Content[i].Board != nil && t.Content[i].Board.Illustration != "" {
illustrations = append(illustrations, t.Content[i].Board.Illustration)
}
if t.Content[i].Playbook != nil && t.Content[i].Playbook.Illustration != "" {
illustrations = append(illustrations, t.Content[i].Playbook.Illustration)
}
}
}
code := bytes.NewBuffer(nil)
tmpl, err := template.New("worktemplates").Parse(tpl)
if err != nil {
log.Fatal(err)
}
tmpl.Execute(code, struct {
Templates []WorkTemplateWithMD5
Categories []WorkTemplateCategoryWithMD5
}{
Templates: ts,
Categories: cats,
})
formattedCode, err := imports.Process(path.Join("worktemplate_generated.go"), code.Bytes(), &imports.Options{Comments: true})
if err != nil {
log.Fatal(errors.Wrap(err, "failed to format code"))
}
err = os.WriteFile(path.Join("worktemplate_generated.go"), formattedCode, 0644)
if err != nil {
log.Fatal(err)
}
// order ts by category alphabetically and by name alphabetically
sort.Slice(ts, func(i, j int) bool {
if ts[i].Category == ts[j].Category {
return ts[i].UseCase < ts[j].UseCase
}
return ts[i].Category < ts[j].Category
})
// print all translatable content
fmt.Println("\nTranslation helpers:\n====================")
for _, t := range ts {
translationHelper(t.Description.Board)
translationHelper(t.Description.Channel)
translationHelper(t.Description.Integration)
translationHelper(t.Description.Playbook)
}
fmt.Println("Missing illustrations:")
for _, illustration := range illustrations {
// check if file exists
illusPath := path.Join("../../../../webapp/channels/src/images", illustration[8:])
if _, err := os.Stat(illusPath); os.IsNotExist(err) {
fmt.Println("\t" + illusPath)
}
}
}
var translationHelperTemplate = `{
"id": %q,
"translation": %q
},`
func translationHelper(t *worktemplates.TranslatableString) {
if t != nil && t.ID != "" && t.DefaultMessage != "" {
fmt.Printf(translationHelperTemplate, t.ID, t.DefaultMessage)
fmt.Println("")
}
}
//go:embed worktemplate.tmpl
var tpl string

Просмотреть файл

@@ -1,103 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make generate-worktemplates"
// DO NOT EDIT
package worktemplates
func init() {
{{- range .Categories}}
registerWorkTemplateCategory("{{.ID}}", wtc{{.MD5}})
{{- end -}}
{{range .Templates}}
registerWorkTemplate("{{.ID}}", wt{{.MD5}})
{{- end}}
// Register categories strings
{{range .Categories -}}
_ = T("{{.Name}}")
{{end}}
// Register translation strings
{{range .Templates -}}
{{if and (.Description.Channel) (ne .Description.Channel.ID "")}}_ = T("{{.Description.Channel.ID}}")
{{end -}}
{{if and (.Description.Board) (ne .Description.Board.ID "")}}_ = T("{{.Description.Board.ID}}")
{{end -}}
{{if and (.Description.Playbook) (ne .Description.Playbook.ID "")}}_ = T("{{.Description.Playbook.ID}}")
{{end -}}
{{if and (.Description.Integration) (ne .Description.Integration.ID "")}}_ = T("{{.Description.Integration.ID}}")
{{end -}}
{{end -}}
}
{{range .Categories}}
var wtc{{.MD5}} = &WorkTemplateCategory{
ID: "{{.ID}}",
Name: "{{.Name}}",
}
{{end}}
{{range .Templates}}
var wt{{.MD5}} = &WorkTemplate{
ID: "{{.ID}}",
Category: "{{.Category}}",
UseCase: "{{.UseCase}}",
Illustration: "{{.Illustration}}",
Visibility: "{{.Visibility}}",
OnboardingOnly: {{.OnboardingOnly}},
{{if .FeatureFlag}}FeatureFlag: &FeatureFlag{
Name: "{{.FeatureFlag.Name}}",
Value: "{{.FeatureFlag.Value}}",
},{{end}}
Description: Description{
{{if .Description.Channel}}Channel: &TranslatableString{
ID: "{{.Description.Channel.ID}}",
DefaultMessage: "{{.Description.Channel.DefaultMessage}}",
Illustration: "{{.Description.Channel.Illustration}}",
},{{end}}
{{if .Description.Board}}Board: &TranslatableString{
ID: "{{.Description.Board.ID}}",
DefaultMessage: "{{.Description.Board.DefaultMessage}}",
Illustration: "{{.Description.Board.Illustration}}",
},{{end}}
{{if .Description.Playbook}}Playbook: &TranslatableString{
ID: "{{.Description.Playbook.ID}}",
DefaultMessage: "{{.Description.Playbook.DefaultMessage}}",
Illustration: "{{.Description.Playbook.Illustration}}",
},{{end}}
{{if .Description.Integration}}Integration: &TranslatableString{
ID: "{{.Description.Integration.ID}}",
DefaultMessage: "{{.Description.Integration.DefaultMessage}}",
Illustration: "{{.Description.Integration.Illustration}}",
},{{end}}
},
Content: []Content{
{{range .Content}}{
{{if .Channel}}Channel: &Channel{
ID: "{{.Channel.ID}}",
Name: "{{.Channel.Name}}",
Purpose: "{{.Channel.Purpose}}",
Playbook: "{{.Channel.Playbook}}",
Illustration: "{{.Channel.Illustration}}",
},{{end}}{{if .Board}}Board: &Board{
ID: "{{.Board.ID}}",
Template: "{{.Board.Template}}",
Name: "{{.Board.Name}}",
Channel: "{{.Board.Channel}}",
Illustration: "{{.Board.Illustration}}",
},{{end}}{{if .Playbook}}Playbook: &Playbook{
Template: "{{.Playbook.Template}}",
Name: "{{.Playbook.Name}}",
ID: "{{.Playbook.ID}}",
Illustration: "{{.Playbook.Illustration}}",
},{{end}}{{if .Integration}}Integration: &Integration{
ID: "{{.Integration.ID}}",
Recommended: {{.Integration.Recommended}},
},{{end}}
},
{{end}}
},
}
{{end}}

Просмотреть файл

@@ -1,106 +0,0 @@
// 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-server/server/v8/playbooks/client"
"github.com/mattermost/mattermost-server/server/public/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 {
License *model.License
// 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)
}
// private playbook is an E20/Enterprise feature
if !public && (p.License == nil || (p.License.SkuShortName != model.LicenseShortSkuE20 && p.License.SkuShortName != model.LicenseShortSkuEnterprise)) {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.license_cannot_create_private_playbook", 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"`
}

Просмотреть файл

@@ -1,110 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package worktemplates
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/server/public/model"
pbclient "github.com/mattermost/mattermost-server/server/v8/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("cannot create private playbook if the license is not enterprise", func(t *testing.T) {
wtcrMod := *wtcr
wtcrMod.Visibility = model.WorkTemplateVisibilityPrivate
appErr := wtcrMod.CanBeExecuted(PermissionSet{
License: model.NewTestLicenseSKU(model.LicenseShortSkuProfessional, ""),
CanCreatePrivateChannel: true,
CanCreatePrivateBoard: true,
CanCreatePrivatePlaybook: true,
CanCreatePublicChannel: true, // needed for the channel run
})
require.NotNil(t, appErr)
appErr = wtcrMod.CanBeExecuted(PermissionSet{
License: nil,
CanCreatePrivateChannel: true,
CanCreatePrivateBoard: true,
CanCreatePrivatePlaybook: true,
CanCreatePublicChannel: true,
})
require.NotNil(t, appErr)
// enterprise and E20 ok
appErr = wtcrMod.CanBeExecuted(PermissionSet{
License: model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise, ""),
CanCreatePrivateChannel: true,
CanCreatePrivateBoard: true,
CanCreatePrivatePlaybook: true,
CanCreatePublicChannel: true,
})
require.Nil(t, appErr)
appErr = wtcrMod.CanBeExecuted(PermissionSet{
License: model.NewTestLicenseSKU(model.LicenseShortSkuE20, ""),
CanCreatePrivateChannel: true,
CanCreatePrivateBoard: true,
CanCreatePrivatePlaybook: true,
CanCreatePublicChannel: true,
})
require.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)
})
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -1,348 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package worktemplates
import (
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/public/shared/i18n"
)
type WorkTemplateCategory struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
}
type WorkTemplate struct {
ID string `yaml:"id"`
Category string `yaml:"category"`
UseCase string `yaml:"useCase"`
Illustration string `yaml:"illustration"`
Visibility string `yaml:"visibility"`
OnboardingOnly bool `yaml:"onboardingOnly"`
FeatureFlag *FeatureFlag `yaml:"featureFlag,omitempty"`
Description Description `yaml:"description"`
Content []Content `yaml:"content"`
}
func (wt WorkTemplate) ToModelWorkTemplate(t i18n.TranslateFunc) *model.WorkTemplate {
mwt := &model.WorkTemplate{
ID: wt.ID,
Category: wt.Category,
UseCase: wt.UseCase,
Illustration: wt.Illustration,
Visibility: wt.Visibility,
}
if wt.FeatureFlag != nil {
mwt.FeatureFlag = &model.WorkTemplateFeatureFlag{
Name: wt.FeatureFlag.Name,
Value: wt.FeatureFlag.Value,
}
}
if wt.Description.Channel != nil {
mwt.Description.Channel = &model.DescriptionContent{
Message: wt.Description.Channel.Translate(t),
Illustration: wt.Description.Channel.Illustration,
}
}
if wt.Description.Board != nil {
mwt.Description.Board = &model.DescriptionContent{
Message: wt.Description.Board.Translate(t),
Illustration: wt.Description.Board.Illustration,
}
}
if wt.Description.Playbook != nil {
mwt.Description.Playbook = &model.DescriptionContent{
Message: wt.Description.Playbook.Translate(t),
Illustration: wt.Description.Playbook.Illustration,
}
}
if wt.Description.Integration != nil {
mwt.Description.Integration = &model.DescriptionContent{
Message: wt.Description.Integration.Translate(t),
Illustration: wt.Description.Integration.Illustration,
}
}
for _, content := range wt.Content {
if content.Channel != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Channel: &model.WorkTemplateChannel{
ID: content.Channel.ID,
Name: content.Channel.Name,
Purpose: content.Channel.Purpose,
Playbook: content.Channel.Playbook,
Illustration: content.Channel.Illustration,
},
})
}
if content.Board != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Board: &model.WorkTemplateBoard{
ID: content.Board.ID,
Name: content.Board.Name,
Template: content.Board.Template,
Channel: content.Board.Channel,
Illustration: content.Board.Illustration,
},
})
}
if content.Playbook != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Playbook: &model.WorkTemplatePlaybook{
ID: content.Playbook.ID,
Name: content.Playbook.Name,
Template: content.Playbook.Template,
Illustration: content.Playbook.Illustration,
},
})
}
if content.Integration != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Integration: &model.WorkTemplateIntegration{
ID: content.Integration.ID,
Recommended: content.Integration.Recommended,
},
})
}
}
return mwt
}
func (wt WorkTemplate) Validate(categoryIds map[string]struct{}) error {
if wt.ID == "" {
return errors.New("id is required")
}
if wt.Category == "" {
return errors.New("category is required")
}
if _, ok := categoryIds[wt.Category]; !ok {
return fmt.Errorf("category %s does not exist", wt.Category)
}
if wt.UseCase == "" {
return errors.New("useCase is required")
}
if wt.Illustration == "" {
return errors.New("illustration is required")
}
if wt.Visibility == "" {
return errors.New("visibility is required")
}
hasChannel := false
hasBoard := false
hasPlaybook := false
hasIntegration := false
foundChannels := map[string]struct{}{}
foundPlaybooks := map[string]struct{}{}
foundBoards := map[string]struct{}{}
foundIntegrations := map[string]struct{}{}
mustHaveChannels := []string{}
mustHavePlaybooks := []string{}
currentIdx := 0
for _, content := range wt.Content {
if content.Channel != nil {
hasChannel = true
if cErr := content.Channel.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundChannels[content.Channel.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate channel %s found", content.Channel.ID), currentIdx)
}
foundChannels[content.Channel.ID] = struct{}{}
if content.Channel.Playbook != "" {
mustHavePlaybooks = append(mustHavePlaybooks, content.Channel.Playbook)
}
}
if content.Board != nil {
hasBoard = true
if cErr := content.Board.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundBoards[content.Board.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate board %s found", content.Board.ID), currentIdx)
}
foundBoards[content.Board.ID] = struct{}{}
if content.Board.Channel != "" {
mustHaveChannels = append(mustHaveChannels, content.Board.Channel)
}
}
if content.Playbook != nil {
hasPlaybook = true
if cErr := content.Playbook.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundPlaybooks[content.Playbook.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate playbook %s found", content.Playbook.ID), currentIdx)
}
foundPlaybooks[content.Playbook.ID] = struct{}{}
}
if content.Integration != nil {
hasIntegration = true
if cErr := content.Integration.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundIntegrations[content.Integration.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate integration %s found", content.Integration.ID), currentIdx)
}
foundIntegrations[content.Integration.ID] = struct{}{}
}
}
if !wt.OnboardingOnly {
if hasChannel && wt.Description.Channel == nil {
return errors.New("description.channel is required")
}
if hasBoard && wt.Description.Board == nil {
return errors.New("description.board is required")
}
if hasPlaybook && wt.Description.Playbook == nil {
return errors.New("description.playbook is required")
}
if hasIntegration && wt.Description.Integration == nil {
return errors.New("description.integration is required")
}
}
for _, channel := range mustHaveChannels {
if _, ok := foundChannels[channel]; !ok {
return fmt.Errorf("channel %s is required", channel)
}
}
for _, playbook := range mustHavePlaybooks {
if _, ok := foundPlaybooks[playbook]; !ok {
return fmt.Errorf("playbook %s is required", playbook)
}
}
return nil
}
type FeatureFlag struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
}
type TranslatableString struct {
ID string `yaml:"id"`
DefaultMessage string `yaml:"defaultMessage"`
Illustration string `yaml:"illustration"`
}
func (ts TranslatableString) Translate(t i18n.TranslateFunc) string {
if ts.ID != "" {
msg := t(ts.ID)
if msg != ts.ID && msg != "" {
return msg
}
}
return ts.DefaultMessage
}
type Description struct {
Channel *TranslatableString `yaml:"channel"`
Board *TranslatableString `yaml:"board"`
Playbook *TranslatableString `yaml:"playbook"`
Integration *TranslatableString `yaml:"integration"`
}
type Channel struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Purpose string `yaml:"purpose"`
Playbook string `yaml:"playbook"`
Illustration string `yaml:"illustration"`
}
func (c *Channel) Validate() error {
if c.ID == "" {
return errors.New("id is required")
}
if c.Name == "" {
return errors.New("name is required")
}
return nil
}
type Board struct {
ID string `yaml:"id"`
Template string `yaml:"template"`
Name string `yaml:"name"`
Channel string `yaml:"channel"`
Illustration string `yaml:"illustration"`
}
func (b Board) Validate() error {
if b.ID == "" {
return errors.New("id is required")
}
if b.Template == "" {
return errors.New("template is required")
}
if b.Name == "" {
return errors.New("name is required")
}
return nil
}
type Playbook struct {
Template string `yaml:"template"`
Name string `yaml:"name"`
ID string `yaml:"id"`
Illustration string `yaml:"illustration"`
}
func (p *Playbook) Validate() error {
if p.ID == "" {
return errors.New("id is required")
}
if p.Template == "" {
return errors.New("template is required")
}
if p.Name == "" {
return errors.New("name is required")
}
return nil
}
type Integration struct {
ID string `yaml:"id"`
Recommended bool `yaml:"recommended"`
}
func (i *Integration) Validate() error {
if i.ID == "" {
return errors.New("id is required")
}
return nil
}
type Content struct {
Channel *Channel `yaml:"channel,omitempty"`
Board *Board `yaml:"board,omitempty"`
Playbook *Playbook `yaml:"playbook,omitempty"`
Integration *Integration `yaml:"integration,omitempty"`
}
func wrapContentError(err error, index int) error {
return errors.Wrapf(err, "content #%d validation failed", index)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -1,41 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate go run generator/main.go
package worktemplates
var OrderedWorkTemplates = []*WorkTemplate{}
var OrderedWorkTemplateCategories = []*WorkTemplateCategory{}
// T is a placeholder to allow the translation tool to register the strings
func T(id string) string {
return id
}
func registerWorkTemplate(id string, wt *WorkTemplate) {
OrderedWorkTemplates = append(OrderedWorkTemplates, wt)
}
func registerWorkTemplateCategory(id string, wtc *WorkTemplateCategory) {
OrderedWorkTemplateCategories = append(OrderedWorkTemplateCategories, wtc)
}
func ListCategories() ([]*WorkTemplateCategory, error) {
return OrderedWorkTemplateCategories, nil
}
func ListByCategory(category string, includeOnboardingTemplate bool) ([]*WorkTemplate, error) {
wts := []*WorkTemplate{}
for i := range OrderedWorkTemplates {
if OrderedWorkTemplates[i].Category == category {
// do not include work template with onboarding only flag if includeOnboardingTemplate is false
if !includeOnboardingTemplate && OrderedWorkTemplates[i].OnboardingOnly {
continue
}
wts = append(wts, OrderedWorkTemplates[i])
}
}
return wts, nil
}