[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 удалений

20
.github/workflows/server-ci-template.yml поставляемый
Просмотреть файл

@@ -151,26 +151,6 @@ jobs:
git checkout $GITHUB_HEAD_REF || git checkout $GITHUB_BASE_REF || true
make build
cd ../mattermost
check-generate-work-templates:
name: Generate work templates
runs-on: ubuntu-22.04
defaults:
run:
working-directory: server
steps:
- name: Checkout mattermost project
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Setup Go
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
with:
go-version: ${{ env.go-version }}
cache-dependency-path: |
server/go.sum
server/public/go.sum
- name: Generate work templates
run: make generate-worktemplates
- name: Check generated work templates
run: if [[ -n $(git status --porcelain) ]]; then echo "Please update the worktemplates using make generate-worktemplates"; exit 1; fi
check-email-templates:
name: Generate email templates
runs-on: ubuntu-22.04

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

@@ -674,7 +674,6 @@ const defaultServerConfig: AdminConfig = {
InsightsEnabled: true,
CommandPalette: false,
SendWelcomePost: true,
WorkTemplate: true,
PostPriority: true,
WysiwygEditor: false,
PeopleProduct: false,

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

@@ -354,9 +354,6 @@ telemetry-mocks: ## Creates mock files.
store-layers: ## Generate layers for the store
$(GO) generate $(GOFLAGS) ./channels/store
generate-worktemplates: ## Generate work templates
$(GO) generate $(GOFLAGS) ./channels/app/worktemplates
new-migration: ## Creates a new migration. Run with make new-migration name=<>
$(GO) install github.com/mattermost/morph/cmd/morph@master
@echo "Generating new migration for mysql"
@@ -395,7 +392,6 @@ sharedchannel-mocks: ## Creates mock files for shared channels.
misc-mocks: ## Creates mocks for misc interfaces.
$(GO) install github.com/vektra/mockery/v2/...@v2.23.2
$(GOBIN)/mockery --dir channels/utils --name LicenseValidatorIface --output channels/utils/mocks --note 'Regenerate this file using `make misc-mocks`.'
$(GOBIN)/mockery --dir channels/app --name WorkTemplateExecutor --output channels/app/mocks --note 'Regenerate this file using `make misc-mocks`.'
email-mocks: ## Creates mocks for misc interfaces.
$(GO) install github.com/vektra/mockery/v2/...@v2.23.2

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

@@ -1,60 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package integrationtests
import (
"testing"
"github.com/stretchr/testify/require"
)
// This test is there to guarantee that the board templates needed for
// the work template are present in the default templates.
// If this fails, you might need to sync with the channels team.
func TestGetTemplatesForWorkTemplate(t *testing.T) {
// map[name]trackingTemplateId
knownInWorkTemplates := map[string]string{
"Company Goals & OKRs": "7ba22ccfdfac391d63dea5c4b8cde0de",
"Competitive Analysis": "06f4bff367a7c2126fab2380c9dec23c",
"Content Calendar": "c75fbd659d2258b5183af2236d176ab4",
"Meeting Agenda ": "54fcf9c610f0ac5e4c522c0657c90602",
"Personal Goals ": "7f32dc8d2ae008cfe56554e9363505cc",
"Personal Tasls ": "dfb70c146a4584b8a21837477c7b5431",
"Project Tasks ": "a4ec399ab4f2088b1051c3cdf1dde4c3",
"Roadmap ": "b728c6ca730e2cfc229741c5a4712b65",
"Sales Pipeline CRM": "ecc250bb7dff0fe02247f1110f097544",
"Sprint Planner ": "99b74e26d2f5d0a9b346d43c0a7bfb09",
"Team Retrospective": "e4f03181c4ced8edd4d53d33d569a086",
"User Research Sessions": "6c345c7f50f6833f78b7d0f08ce450a3",
}
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
err := th.Server.App().InitTemplates()
require.NoError(t, err, "InitTemplates should not fail")
rBoards, resp := th.Client.GetTemplatesForTeam("0")
th.CheckOK(resp)
require.NotNil(t, rBoards)
trackingTemplateIDs := []string{}
for _, board := range rBoards {
property, _ := board.GetPropertyString("trackingTemplateId")
if property != "" {
trackingTemplateIDs = append(trackingTemplateIDs, property)
}
}
// make sure all known templates are in trackingTemplateIds
for name, ttID := range knownInWorkTemplates {
found := false
for _, trackingTemplateID := range trackingTemplateIDs {
if trackingTemplateID == ttID {
found = true
break
}
}
require.True(t, found, "trackingTemplateId %s for %s not found", ttID, name)
}
}

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

@@ -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
}

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

@@ -92,7 +92,6 @@ require (
gopkg.in/mail.v2 v2.3.1
gopkg.in/olivere/elastic.v6 v6.2.37
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -232,6 +231,7 @@ require (
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect

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

@@ -1482,18 +1482,6 @@
"id": "api.command_shrug.name",
"translation": "shrug"
},
{
"id": "api.command_templates.desc",
"translation": "Open the create from template window"
},
{
"id": "api.command_templates.name",
"translation": "templates"
},
{
"id": "api.command_templates.unsupported.app_error",
"translation": "The templates command is not supported on your device."
},
{
"id": "api.config.client.old_format.app_error",
"translation": "New format for the client configuration is not supported yet. Please specify format=old in the query string."
@@ -4555,10 +4543,6 @@
"id": "api.websocket_handler.server_busy.app_error",
"translation": "Server is busy, non-critical services are temporarily unavailable."
},
{
"id": "api.work_templates.disabled",
"translation": "Work templates are disabled."
},
{
"id": "app.acknowledgement.delete.app_error",
"translation": "Unable to delete acknowledgement."
@@ -7335,66 +7319,6 @@
"id": "app.webhooks.update_outgoing.app_error",
"translation": "Unable to update the webhook."
},
{
"id": "app.worktemplate.execution_request.cannot_create_private_board",
"translation": "You don't have permissions to create a private board."
},
{
"id": "app.worktemplate.execution_request.cannot_create_private_channel",
"translation": "You don't have permissions to create a private channel."
},
{
"id": "app.worktemplate.execution_request.cannot_create_private_playbook",
"translation": "You don't have permissions to create a private playbook."
},
{
"id": "app.worktemplate.execution_request.cannot_create_public_board",
"translation": "You don't have permissions to create a public board."
},
{
"id": "app.worktemplate.execution_request.cannot_create_public_channel",
"translation": "You don't have permissions to create a public channel."
},
{
"id": "app.worktemplate.execution_request.cannot_create_public_playbook",
"translation": "You don't have permissions to create a public playbook."
},
{
"id": "app.worktemplate.execution_request.license_cannot_create_private_playbook",
"translation": "Your license does not support private playbooks."
},
{
"id": "app.worktemplates.execute_work_template.app_error",
"translation": "Error while executing a work template."
},
{
"id": "app.worktemplates.execute_work_template.boards.create_error",
"translation": "Error while creating a board."
},
{
"id": "app.worktemplates.execute_work_template.channels.create_error",
"translation": "Error while creating a channel."
},
{
"id": "app.worktemplates.execute_work_template.name_too_long",
"translation": "The name field cannot contain more than 64 characters."
},
{
"id": "app.worktemplates.execute_work_template.playbooks.create_error",
"translation": "Error while creating a playbook."
},
{
"id": "app.worktemplates.execute_work_template.playbooks.find_channel_error",
"translation": "Unable to find channel associated with a playbook."
},
{
"id": "app.worktemplates.get_categories.app_error",
"translation": "Unable to get work template categories"
},
{
"id": "app.worktemplates.get_templates.app_error",
"translation": "Unable to get work templates"
},
{
"id": "bleveengine.already_started.error",
"translation": "Bleve is already started."
@@ -10122,557 +10046,5 @@
{
"id": "web.incoming_webhook.user.app_error",
"translation": "Couldn't find the user."
},
{
"id": "worktemplate.category.design",
"translation": "Design"
},
{
"id": "worktemplate.category.devops",
"translation": "DevOps"
},
{
"id": "worktemplate.category.engineering",
"translation": "Engineering"
},
{
"id": "worktemplate.category.leadership",
"translation": "Leadership"
},
{
"id": "worktemplate.category.marketing",
"translation": "Marketing"
},
{
"id": "worktemplate.category.other",
"translation": "Other"
},
{
"id": "worktemplate.category.product_teams",
"translation": "Product"
},
{
"id": "worktemplate.category.project_management",
"translation": "Project Management"
},
{
"id": "worktemplate.category.qa",
"translation": "QA"
},
{
"id": "worktemplate.design.content_calendar.board",
"translation": "Use the Content Calendar boad to track ideas, plan your content themes, manage the creation process, and set milestones."
},
{
"id": "worktemplate.design.content_calendar.channel",
"translation": "Share content ideas, trending posts, blog links, and mentions in a dedicated channel."
},
{
"id": "worktemplate.design.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "worktemplate.design.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it, in a collaborative channel."
},
{
"id": "worktemplate.design.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.design.feature_release.description.board",
"translation": "Keep meetings on track with the Meeting Agenda board. Manage your workload with the Project Tasks board."
},
{
"id": "worktemplate.design.feature_release.description.channel",
"translation": "Chat with your team about any release blockers and changes in a channel that connects easily with your boards, playbooks and other integrations."
},
{
"id": "worktemplate.design.feature_release.description.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools to support your feature release, such as GitHub. These will be downloaded for you."
},
{
"id": "worktemplate.design.feature_release.description.playbook",
"translation": "Boost cross-functional team collaboration with task checklists and automation that support your feature development process. When youre done, run a retrospective and make improvements for your next release."
},
{
"id": "worktemplate.design.product_release.board",
"translation": "Use the Product Release board to support your release timeframe and process, ensuring everyone knows which tasks are due."
},
{
"id": "worktemplate.design.product_release.channel",
"translation": "Chat with your team about daily milestones, any blockers, and changes to deliverables, easily and quickly."
},
{
"id": "worktemplate.design.product_release.playbook",
"translation": "Create repeatable workflows that are easy to follow and implement so product releases are reliable and on time."
},
{
"id": "worktemplate.design.sprint_planning.board",
"translation": "Track your team's progress toward weekly goals with sprint breakdowns, prioritization, owner assignment, and comments."
},
{
"id": "worktemplate.design.sprint_planning.channel",
"translation": "Chat with your team in a channel that connects easily with your boards and integrations."
},
{
"id": "worktemplate.design.sprint_planning.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom. These will be downloaded for you."
},
{
"id": "worktemplate.devops.bug_bash.channel",
"translation": "Plan and manage bug reports and resolutions in a single channel, thats easily accessible to your team and organization."
},
{
"id": "worktemplate.devops.bug_bash.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Jira, to track your bug bash progress. These will be downloaded for you."
},
{
"id": "worktemplate.devops.bug_bash.playbook",
"translation": "Use checklists to assign testing areas and automated tasks to run a comprehensive bug bash process. Use a retrospective to review your process and improve it for next time."
},
{
"id": "worktemplate.devops.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "worktemplate.devops.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it, in a collaborative channel."
},
{
"id": "worktemplate.devops.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.devops.incident_resolution.description.board",
"translation": "Use the Incident Resolution board to support repeatable processes and assign defined tasks across the team."
},
{
"id": "worktemplate.devops.incident_resolution.description.channel",
"translation": "Chat with your team about priorities, add stakeholders, provide updates, and work toward a resolution in a single channel."
},
{
"id": "worktemplate.devops.incident_resolution.description.playbook",
"translation": "Use checklists and automation to bring in key team members, and share how your incident is tracking toward resolution."
},
{
"id": "worktemplate.devops.product_release.board",
"translation": "Use the Product Release board to support your release timeframe and process, ensuring everyone knows which tasks are due."
},
{
"id": "worktemplate.devops.product_release.channel",
"translation": "Chat with your team about daily milestones, any blockers, and changes to deliverables, easily and quickly."
},
{
"id": "worktemplate.devops.product_release.playbook",
"translation": "Create repeatable workflows that are easy to follow and implement so product releases are reliable and on time."
},
{
"id": "worktemplate.devops.sprint_planning.board",
"translation": "Track your team's progress toward weekly goals with sprint breakdowns, prioritization, owner assignment, and comments."
},
{
"id": "worktemplate.devops.sprint_planning.channel",
"translation": "Chat with your team in a channel that connects easily with your boards and integrations."
},
{
"id": "worktemplate.devops.sprint_planning.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom. These will be downloaded for you."
},
{
"id": "worktemplate.engineering.bug_bash.channel",
"translation": "Plan and manage bug reports and resolutions in a single channel, thats easily accessible to your team and organization."
},
{
"id": "worktemplate.engineering.bug_bash.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Jira, to track your bug bash progress. These will be downloaded for you."
},
{
"id": "worktemplate.engineering.bug_bash.playbook",
"translation": "Use checklists to assign testing areas and automated tasks to run a comprehensive bug bash process. Use a retrospective to review your process and improve it for next time."
},
{
"id": "worktemplate.engineering.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "worktemplate.engineering.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it, in a collaborative channel."
},
{
"id": "worktemplate.engineering.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.engineering.feature_release.description.board",
"translation": "Keep meetings on track with the Meeting Agenda board. Manage your workload with the Project Tasks board."
},
{
"id": "worktemplate.engineering.feature_release.description.channel",
"translation": "Chat with your team about any release blockers and changes in a channel that connects easily with your boards, playbooks and other integrations."
},
{
"id": "worktemplate.engineering.feature_release.description.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools to support your feature release, such as GitHub. These will be downloaded for you."
},
{
"id": "worktemplate.engineering.feature_release.description.playbook",
"translation": "Boost cross-functional team collaboration with task checklists and automation that support your feature development process. When youre done, run a retrospective and make improvements for your next release."
},
{
"id": "worktemplate.engineering.goals_and_okrs.board",
"translation": "Track your team's progress toward organizational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.engineering.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time, and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.engineering.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom, to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.engineering.sprint_planning.board",
"translation": "Track your team's progress toward weekly goals with sprint breakdowns, prioritization, owner assignment, and comments."
},
{
"id": "worktemplate.engineering.sprint_planning.channel",
"translation": "Chat with your team in a channel that connects easily with your boards and integrations."
},
{
"id": "worktemplate.engineering.sprint_planning.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom. These will be downloaded for you."
},
{
"id": "worktemplate.leadership.content_calendar.board",
"translation": "Use the Content Calendar boad to track ideas, plan your content themes, manage the creation process, and set milestones."
},
{
"id": "worktemplate.leadership.content_calendar.channel",
"translation": "Share content ideas, trending posts, blog links, and mentions in a dedicated channel."
},
{
"id": "worktemplate.leadership.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "worktemplate.leadership.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it, in a collaborative channel."
},
{
"id": "worktemplate.leadership.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.leadership.goals_and_okrs.board",
"translation": "Track your team's progress toward organizational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.leadership.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time, and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.leadership.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom, to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.leadership.incident_resolution.description.board",
"translation": "Use the Incident Resolution board to support repeatable processes and assign defined tasks across the team."
},
{
"id": "worktemplate.leadership.incident_resolution.description.channel",
"translation": "Chat with your team about priorities, add stakeholders, provide updates, and work toward a resolution in a single channel."
},
{
"id": "worktemplate.leadership.incident_resolution.description.playbook",
"translation": "Use checklists and automation to bring in key team members, and share how your incident is tracking toward resolution."
},
{
"id": "worktemplate.marketing.content_calendar.board",
"translation": "Use the Content Calendar boad to track ideas, plan your content themes, manage the creation process, and set milestones."
},
{
"id": "worktemplate.marketing.content_calendar.channel",
"translation": "Share content ideas, trending posts, blog links, and mentions in a dedicated channel."
},
{
"id": "worktemplate.marketing.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "worktemplate.marketing.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it, in a collaborative channel."
},
{
"id": "worktemplate.marketing.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.marketing.goals_and_okrs.board",
"translation": "Track your team's progress toward organizational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.marketing.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time, and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.marketing.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom, to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.marketing.product_release.board",
"translation": "Use the Product Release board to support your release timeframe and process, ensuring everyone knows which tasks are due."
},
{
"id": "worktemplate.marketing.product_release.channel",
"translation": "Chat with your team about daily milestones, any blockers, and changes to deliverables, easily and quickly."
},
{
"id": "worktemplate.marketing.product_release.playbook",
"translation": "Create repeatable workflows that are easy to follow and implement so product releases are reliable and on time."
},
{
"id": "worktemplate.other.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "worktemplate.other.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it, in a collaborative channel."
},
{
"id": "worktemplate.other.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.other.feature_release.description.board",
"translation": "Keep meetings on track with the Meeting Agenda board. Manage your workload with the Project Tasks board."
},
{
"id": "worktemplate.other.feature_release.description.channel",
"translation": "Chat with your team about any release blockers and changes in a channel that connects easily with your boards, playbooks and other integrations."
},
{
"id": "worktemplate.other.feature_release.description.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools to support your feature release, such as GitHub. These will be downloaded for you."
},
{
"id": "worktemplate.other.feature_release.description.playbook",
"translation": "Boost cross-functional team collaboration with task checklists and automation that support your feature development process. When youre done, run a retrospective and make improvements for your next release."
},
{
"id": "worktemplate.other.goals_and_okrs.board",
"translation": "Track your team's progress toward organizational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.other.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time, and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.other.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom, to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.other.incident_resolution.description.board",
"translation": "Use the Incident Resolution board to support repeatable processes and assign defined tasks across the team."
},
{
"id": "worktemplate.other.incident_resolution.description.channel",
"translation": "Chat with your team about priorities, add stakeholders, provide updates, and work toward a resolution in a single channel."
},
{
"id": "worktemplate.other.incident_resolution.description.playbook",
"translation": "Use checklists and automation to bring in key team members, and share how your incident is tracking toward resolution."
},
{
"id": "worktemplate.other.product_release.board",
"translation": "Use the Product Release board to support your release timeframe and process, ensuring everyone knows which tasks are due."
},
{
"id": "worktemplate.other.product_release.channel",
"translation": "Chat with your team about daily milestones, any blockers, and changes to deliverables, easily and quickly."
},
{
"id": "worktemplate.other.product_release.playbook",
"translation": "Create repeatable workflows that are easy to follow and implement so product releases are reliable and on time."
},
{
"id": "worktemplate.product_teams.bug_bash.channel",
"translation": "Plan and manage bug reports and resolutions in a single channel, thats easily accessible to your team and organization."
},
{
"id": "worktemplate.product_teams.bug_bash.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Jira, to track your bug bash progress. These will be downloaded for you."
},
{
"id": "worktemplate.product_teams.bug_bash.playbook",
"translation": "Use checklists to assign testing areas and automated tasks to run a comprehensive bug bash process. Use a retrospective to review your process and improve it for next time."
},
{
"id": "worktemplate.product_teams.feature_release.description.board",
"translation": "Keep meetings on track with the Meeting Agenda board. Manage your workload with the Project Tasks board."
},
{
"id": "worktemplate.product_teams.feature_release.description.channel",
"translation": "Chat with your team about any release blockers and changes in a channel that connects easily with your boards, playbooks and other integrations."
},
{
"id": "worktemplate.product_teams.feature_release.description.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools to support your feature release, such as GitHub. These will be downloaded for you."
},
{
"id": "worktemplate.product_teams.feature_release.description.playbook",
"translation": "Boost cross-functional team collaboration with task checklists and automation that support your feature development process. When youre done, run a retrospective and make improvements for your next release."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.board",
"translation": "Track your team's progress toward organizational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time, and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom, to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.product_teams.product_roadmap.board",
"translation": "Use the Product Roadmap board to manage user feedback, assign resources, view deliverables in a calendar view, and prioritize issues."
},
{
"id": "worktemplate.product_teams.product_roadmap.channel",
"translation": "Chat with your team about your customers' feedback, prioritization, and get aligned on progress together."
},
{
"id": "worktemplate.product_teams.sprint_planning.board",
"translation": "Track your team's progress toward weekly goals with sprint breakdowns, prioritization, owner assignment, and comments."
},
{
"id": "worktemplate.product_teams.sprint_planning.channel",
"translation": "Chat with your team in a channel that connects easily with your boards and integrations."
},
{
"id": "worktemplate.product_teams.sprint_planning.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom. These will be downloaded for you."
},
{
"id": "worktemplate.project_management.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "worktemplate.project_management.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it, in a collaborative channel."
},
{
"id": "worktemplate.project_management.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.project_management.feature_release.description.board",
"translation": "Keep meetings on track with the Meeting Agenda board. Manage your workload with the Project Tasks board."
},
{
"id": "worktemplate.project_management.feature_release.description.channel",
"translation": "Chat with your team about any release blockers and changes in a channel that connects easily with your boards, playbooks and other integrations."
},
{
"id": "worktemplate.project_management.feature_release.description.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools to support your feature release, such as GitHub. These will be downloaded for you."
},
{
"id": "worktemplate.project_management.feature_release.description.playbook",
"translation": "Boost cross-functional team collaboration with task checklists and automation that support your feature development process. When youre done, run a retrospective and make improvements for your next release."
},
{
"id": "worktemplate.project_management.goals_and_okrs.board",
"translation": "Track your team's progress toward organizational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.project_management.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time, and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.project_management.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom, to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.project_management.product_release.board",
"translation": "Use the Product Release board to support your release timeframe and process, ensuring everyone knows which tasks are due."
},
{
"id": "worktemplate.project_management.product_release.channel",
"translation": "Chat with your team about daily milestones, any blockers, and changes to deliverables, easily and quickly."
},
{
"id": "worktemplate.project_management.product_release.playbook",
"translation": "Create repeatable workflows that are easy to follow and implement so product releases are reliable and on time."
},
{
"id": "worktemplate.project_management.product_roadmap.board",
"translation": "Use the Product Roadmap board to manage user feedback, assign resources, view deliverables in a calendar view, and prioritize issues."
},
{
"id": "worktemplate.project_management.product_roadmap.channel",
"translation": "Chat with your team about your customers' feedback, prioritization, and get aligned on progress together."
},
{
"id": "worktemplate.qa.bug_bash.channel",
"translation": "Plan and manage bug reports and resolutions in a single channel, thats easily accessible to your team and organization."
},
{
"id": "worktemplate.qa.bug_bash.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Jira, to track your bug bash progress. These will be downloaded for you."
},
{
"id": "worktemplate.qa.bug_bash.playbook",
"translation": "Use checklists to assign testing areas and automated tasks to run a comprehensive bug bash process. Use a retrospective to review your process and improve it for next time."
},
{
"id": "worktemplate.qa.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "worktemplate.qa.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it, in a collaborative channel."
},
{
"id": "worktemplate.qa.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.qa.incident_resolution.description.board",
"translation": "Use the Incident Resolution board to support repeatable processes and assign defined tasks across the team."
},
{
"id": "worktemplate.qa.incident_resolution.description.channel",
"translation": "Chat with your team about priorities, add stakeholders, provide updates, and work toward a resolution in a single channel."
},
{
"id": "worktemplate.qa.incident_resolution.description.playbook",
"translation": "Use checklists and automation to bring in key team members, and share how your incident is tracking toward resolution."
},
{
"id": "worktemplate.qa.product_release.board",
"translation": "Use the Product Release board to support your release timeframe and process, ensuring everyone knows which tasks are due."
},
{
"id": "worktemplate.qa.product_release.channel",
"translation": "Chat with your team about daily milestones, any blockers, and changes to deliverables, easily and quickly."
},
{
"id": "worktemplate.qa.product_release.playbook",
"translation": "Create repeatable workflows that are easy to follow and implement so product releases are reliable and on time."
},
{
"id": "worktemplate.qa.sprint_planning.board",
"translation": "Track your team's progress toward weekly goals with sprint breakdowns, prioritization, owner assignment, and comments."
},
{
"id": "worktemplate.qa.sprint_planning.channel",
"translation": "Chat with your team in a channel that connects easily with your boards and integrations."
},
{
"id": "worktemplate.qa.sprint_planning.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools, such as Zoom. These will be downloaded for you."
}
]

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

@@ -8773,37 +8773,6 @@ func (c *Client4) CheckCWSConnection(userId string) (*Response, error) {
return BuildResponse(r), nil
}
// Worktemplates sections
func (c *Client4) worktemplatesRoute() string {
return "/worktemplates"
}
// GetWorktemplateCategories returns categories of worktemplates
func (c *Client4) GetWorktemplateCategories() ([]*WorkTemplateCategory, *Response, error) {
r, err := c.DoAPIGet(c.worktemplatesRoute()+"/categories", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var categories []*WorkTemplateCategory
err = json.NewDecoder(r.Body).Decode(&categories)
return categories, BuildResponse(r), err
}
func (c *Client4) GetWorkTemplatesByCategory(category string) ([]*WorkTemplate, *Response, error) {
r, err := c.DoAPIGet(c.worktemplatesRoute()+"/categories/"+category+"/templates", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var templates []*WorkTemplate
err = json.NewDecoder(r.Body).Decode(&templates)
return templates, BuildResponse(r), err
}
func (c *Client4) SubmitTrueUpReview(req map[string]any) (*Response, error) {
reqBytes, err := json.Marshal(req)
if err != nil {

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

@@ -54,8 +54,6 @@ type FeatureFlags struct {
// A/B Test on posting a welcome message
SendWelcomePost bool
WorkTemplate bool
PostPriority bool
// Enable WYSIWYG text editor
@@ -101,7 +99,6 @@ func (f *FeatureFlags) SetDefaults() {
f.SendWelcomePost = true
f.PostPriority = true
f.PeopleProduct = false
f.WorkTemplate = false
f.ReduceOnBoardingTaskList = false
f.ThreadsEverywhere = false
f.GlobalDrafts = true

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

@@ -11,7 +11,6 @@ import (
// CompleteOnboardingRequest describes parameters of the requested plugin.
type CompleteOnboardingRequest struct {
Organization string `json:"organization"` // Organization is the name of the organization
Role string `json:"role"` // Role is the role selected by first admin
InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed
}

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

@@ -1,86 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
const (
// used to assign the work template id to newly created channels
WorkTemplateIDChannelProp = "work_template_id"
// Visibility
WorkTemplateVisibilityPublic = "public"
WorkTemplateVisibilityPrivate = "private"
)
type WorkTemplateCategory struct {
ID string `json:"id"`
Name string `json:"name"`
}
type WorkTemplate struct {
ID string `json:"id"`
Category string `json:"category"`
UseCase string `json:"useCase"`
Illustration string `json:"illustration"`
Visibility string `json:"visibility"`
FeatureFlag *WorkTemplateFeatureFlag `json:"featureFlag,omitempty"`
Description Description `json:"description"`
Content []WorkTemplateContent `json:"content"`
}
type WorkTemplateFeatureFlag struct {
Name string `json:"name"`
Value string `json:"value"`
}
type DescriptionContent struct {
Message string `json:"message"`
Illustration string `json:"illustration"`
}
type Description struct {
Channel *DescriptionContent `json:"channel"`
Board *DescriptionContent `json:"board"`
Playbook *DescriptionContent `json:"playbook"`
Integration *DescriptionContent `json:"integration"`
}
type WorkTemplateChannel struct {
ID string `json:"id"`
Name string `json:"name"`
Purpose string `json:"purpose"`
Playbook string `json:"playbook"`
Illustration string `json:"illustration"`
}
type WorkTemplateBoard struct {
ID string `json:"id"`
Template string `json:"template"`
Name string `json:"name"`
Channel string `json:"channel"`
Illustration string `json:"illustration"`
}
type WorkTemplatePlaybook struct {
Template string `json:"template"`
Name string `json:"name"`
ID string `json:"id"`
Illustration string `json:"illustration"`
}
type WorkTemplateIntegration struct {
ID string `json:"id"`
Recommended bool `json:"recommended"`
}
type WorkTemplateContent struct {
Channel *WorkTemplateChannel `json:"channel,omitempty"`
Board *WorkTemplateBoard `json:"board,omitempty"`
Playbook *WorkTemplatePlaybook `json:"playbook,omitempty"`
Integration *WorkTemplateIntegration `json:"integration,omitempty"`
}
type WorkTemplateExecutionResult struct {
ChannelWithPlaybookIDs []string `json:"channel_with_playbook_ids"`
ChannelIDs []string `json:"channel_ids"`
}

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

@@ -35,11 +35,9 @@ import KeyboardShortcutsModal from 'components/keyboard_shortcuts/keyboard_short
import {GlobalState} from 'types/store';
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
import WorkTemplateModal from 'components/work_templates';
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
import {Permissions} from 'mattermost-redux/constants';
import {isMarketplaceEnabled} from 'mattermost-redux/selectors/entities/general';
import {areWorkTemplatesEnabled} from 'selectors/work_template';
import {doAppSubmit, openAppsModal, postEphemeralCallResponseForCommandArgs} from './apps';
import {trackEvent} from './telemetry_actions';
@@ -140,15 +138,6 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
dispatch(openModal({modalId: ModalIdentifiers.PLUGIN_MARKETPLACE, dialogType: MarketplaceModal, dialogProps: {openedFrom: 'command'}}));
return {data: true};
case '/templates': {
const workTemplateEnabled = areWorkTemplatesEnabled(state);
if (!workTemplateEnabled) {
return {error: {message: localizeMessage('templates_command.disabled', 'Templates are disabled. Please contact your System Administrator for details.')}};
}
dispatch(openModal({modalId: ModalIdentifiers.WORK_TEMPLATE, dialogType: WorkTemplateModal}));
return {data: true};
}
case '/collapse':
case '/expand':
dispatch(PostActions.resetEmbedVisibility());

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

@@ -12,8 +12,6 @@ import {ActionTypes, Constants, ModalIdentifiers} from 'utils/constants';
import {getTeamRedirectChannelIfIsAccesible} from 'actions/global_actions';
import WorkTemplateModal from 'components/work_templates';
import {openModal} from './modals';
export function switchToChannels() {
@@ -47,23 +45,6 @@ export function openInvitationsModal(timeout = 1) {
};
}
export function openWorkTemplateModal(redirectToChannels = true) {
return (dispatch: DispatchFunc) => {
if (redirectToChannels) {
dispatch(switchToChannels());
}
setTimeout(() => {
dispatch(openModal({
modalId: ModalIdentifiers.WORK_TEMPLATE,
dialogType: WorkTemplateModal,
dialogProps: {
},
}));
}, redirectToChannels ? 1000 : 1);
return {data: true};
};
}
export function setShowOnboardingTaskCompletion(open: boolean) {
return {
type: ActionTypes.SHOW_ONBOARDING_TASK_COMPLETION,

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

@@ -7,7 +7,6 @@ export const OnboardingTaskCategory = 'onboarding_task_list';
// Whole task list is based on these
export const OnboardingTasksName = {
CREATE_FROM_WORK_TEMPLATE: 'create_from_work_template',
CHANNELS_TOUR: 'channels_tour',
BOARDS_TOUR: 'boards_tour',
PLAYBOOKS_TOUR: 'playbooks_tour',
@@ -37,7 +36,6 @@ export const GenericTaskSteps = {
};
export const TaskNameMapToSteps = {
[OnboardingTasksName.CREATE_FROM_WORK_TEMPLATE]: GenericTaskSteps,
[OnboardingTasksName.CHANNELS_TOUR]: GenericTaskSteps,
[OnboardingTasksName.BOARDS_TOUR]: GenericTaskSteps,
[OnboardingTasksName.PLAYBOOKS_TOUR]: GenericTaskSteps,

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

@@ -32,7 +32,6 @@ import {isCurrentUserGuestUser, isCurrentUserSystemAdmin, isFirstAdmin} from 'ma
import {GlobalState} from 'types/store';
import {
openInvitationsModal,
openWorkTemplateModal,
setShowOnboardingCompleteProfileTour,
setShowOnboardingVisitConsoleTour,
switchToChannels,
@@ -43,14 +42,12 @@ import {ModalIdentifiers, TELEMETRY_CATEGORIES, ExploreOtherToolsTourSteps} from
import BullsEye from 'components/common/svg_images_components/bulls_eye_svg';
import Channels from 'components/common/svg_images_components/channels_svg';
import Clipboard from 'components/common/svg_images_components/clipboard_svg';
import Newspaper from 'components/common/svg_images_components/newspaper_svg';
import Gears from 'components/common/svg_images_components/gears_svg';
import Handshake from 'components/common/svg_images_components/handshake_svg';
import Phone from 'components/common/svg_images_components/phone_svg';
import Security from 'components/common/svg_images_components/security_svg';
import Sunglasses from 'components/common/svg_images_components/sunglasses_svg';
import Wrench from 'components/common/svg_images_components/wrench_svg';
import {areWorkTemplatesEnabled} from 'selectors/work_template';
import {OnboardingTaskCategory, OnboardingTaskList, OnboardingTasksName, TaskNameMapToSteps} from './constants';
import {generateTelemetryTag} from './utils';
@@ -60,14 +57,6 @@ const getCategory = makeGetCategory();
const useGetTaskDetails = () => {
const {formatMessage} = useIntl();
return {
[OnboardingTasksName.CREATE_FROM_WORK_TEMPLATE]: {
id: 'task_create_from_work_template',
svg: Newspaper,
message: formatMessage({
id: 'onboardingTask.checklist.task_create_from_work_template',
defaultMessage: 'Create from a template.',
}),
},
[OnboardingTasksName.CHANNELS_TOUR]: {
id: 'task_learn_more_about_messaging',
svg: Channels,
@@ -158,7 +147,6 @@ export const useTasksList = () => {
const isThinOnBoardingTaskList = useSelector((state: GlobalState) => {
return isReduceOnBoardingTaskList(state);
});
const workTemplateEnabled = useSelector(areWorkTemplatesEnabled);
// Cloud conditions
const subscription = useSelector((state: GlobalState) => state.entities.cloud.subscription);
@@ -205,10 +193,6 @@ export const useTasksList = () => {
delete list.VISIT_SYSTEM_CONSOLE;
}
if (!workTemplateEnabled) {
delete list.CREATE_FROM_WORK_TEMPLATE;
}
return Object.values(list);
};
@@ -286,12 +270,6 @@ export const useHandleOnBoardingTaskTrigger = () => {
return (taskName: string) => {
switch (taskName) {
case OnboardingTasksName.CREATE_FROM_WORK_TEMPLATE: {
localStorage.setItem(OnboardingTaskCategory, 'true');
dispatch(openWorkTemplateModal(inAdminConsole));
handleSaveData(taskName, TaskNameMapToSteps[taskName].FINISHED, true);
break;
}
case OnboardingTasksName.CHANNELS_TOUR: {
handleSaveData(taskName, TaskNameMapToSteps[taskName].STARTED, true);
const tourCategory = TutorialTourName.ONBOARDING_TUTORIAL_STEP;

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

@@ -15,15 +15,12 @@ import {Team} from '@mattermost/types/teams';
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeam, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
import {getFirstAdminSetupComplete, getConfig, getLicense, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
import {getFirstAdminSetupComplete, getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {Client4} from 'mattermost-redux/client';
import {CategoryOther} from '@mattermost/types/work_templates';
import Constants from 'utils/constants';
import {getSiteURL, teamNameToUrl} from 'utils/url';
import {makeNewTeam} from 'utils/team_utils';
import {GlobalState} from 'types/store';
import {pageVisited, trackEvent} from 'actions/telemetry_actions';
@@ -45,8 +42,6 @@ import {
} from './steps';
import Organization from './organization';
import Roles from './roles';
import RolesIllustration from './roles_illustration';
import Plugins from './plugins';
import Progress from './progress';
import InviteMembers from './invite_members';
@@ -95,7 +90,6 @@ function makeSubmitFail(step: WizardStep) {
const trackSubmitFail = {
[WizardSteps.Organization]: makeSubmitFail(WizardSteps.Organization),
[WizardSteps.Roles]: makeSubmitFail(WizardSteps.Roles),
[WizardSteps.Plugins]: makeSubmitFail(WizardSteps.Plugins),
[WizardSteps.InviteMembers]: makeSubmitFail(WizardSteps.InviteMembers),
[WizardSteps.LaunchingWorkspace]: makeSubmitFail(WizardSteps.LaunchingWorkspace),
@@ -103,7 +97,6 @@ const trackSubmitFail = {
const onPageViews = {
[WizardSteps.Organization]: makeOnPageView(WizardSteps.Organization),
[WizardSteps.Roles]: makeOnPageView(WizardSteps.Roles),
[WizardSteps.Plugins]: makeOnPageView(WizardSteps.Plugins),
[WizardSteps.InviteMembers]: makeOnPageView(WizardSteps.InviteMembers),
[WizardSteps.LaunchingWorkspace]: makeOnPageView(WizardSteps.LaunchingWorkspace),
@@ -118,7 +111,6 @@ const PreparingWorkspace = (props: Props) => {
});
const isUserFirstAdmin = useSelector(isFirstAdmin);
const onboardingFlowEnabled = useSelector(getIsOnboardingFlowEnabled);
const isWorkTemplateEnabled = useSelector((state: GlobalState) => getFeatureFlagValue(state, 'WorkTemplate') === 'true');
const currentTeam = useSelector(getCurrentTeam);
const myTeams = useSelector(getMyTeams);
@@ -136,7 +128,6 @@ const PreparingWorkspace = (props: Props) => {
const stepOrder = [
isSelfHosted && WizardSteps.Organization,
isWorkTemplateEnabled && WizardSteps.Roles,
pluginsEnabled && WizardSteps.Plugins,
WizardSteps.InviteMembers,
WizardSteps.LaunchingWorkspace,
@@ -265,7 +256,6 @@ const PreparingWorkspace = (props: Props) => {
// even if admin skipped submitting plugins.
const completeSetupRequest = {
organization: form.organization,
role: form.role === CategoryOther ? form.roleOther : form.role,
install_plugins: pluginsToSetup,
};
@@ -375,15 +365,6 @@ const PreparingWorkspace = (props: Props) => {
return '';
}, [currentStep]);
const getRolesAnimationClass = useCallback(() => {
if (currentStep === WizardSteps.Roles) {
return 'enter';
} else if (mostRecentStep === WizardSteps.Roles) {
return 'exit';
}
return '';
}, [currentStep]);
let previous: React.ReactNode = (
<div
onClick={goPrevious}
@@ -450,48 +431,6 @@ const PreparingWorkspace = (props: Props) => {
updateTeam={updateTeam}
/>
<Roles
onPageView={onPageViews[WizardSteps.Roles]}
previous={previous}
next={() => {
makeNext(WizardSteps.Roles)({
role: form.role,
roleOther: form.roleOther,
});
}}
quickNext={(role: string) => {
setForm({
...form,
role,
roleOther: '',
});
makeNext(WizardSteps.Roles)({
role,
roleOther: '',
});
}}
skip={() => {
setForm({
...form,
role: '',
roleOther: '',
});
makeNext(WizardSteps.Roles, true)();
}}
transitionDirection={getTransitionDirection(WizardSteps.Roles)}
show={shouldShowPage(WizardSteps.Roles)}
role={form.role}
roleOther={form.roleOther}
setRole={(role: Form['role'], roleOther: Form['roleOther']) => {
setForm({
...form,
role,
roleOther,
});
}}
className='child-page'
/>
<Plugins
isSelfHosted={isSelfHosted}
onPageView={onPageViews[WizardSteps.Plugins]}
@@ -569,9 +508,6 @@ const PreparingWorkspace = (props: Props) => {
<div className={`PreparingWorkspace__invite-members-illustration ${getInviteMembersAnimationClass()}`}>
<InviteMembersIllustration/>
</div>
<div className={`PreparingWorkspace__roles-illustration ${getRolesAnimationClass()}`}>
<RolesIllustration/>
</div>
</div>
);
};

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

@@ -1,47 +0,0 @@
@import 'utils/mixins';
$btn-margin-right: 24px;
.plugins-skip-btn {
margin-left: 8px;
}
.Roles__input {
width: calc(100% - $btn-margin-right);
padding: 10px 16px;
border: 2px solid rgba(var(--center-channel-color-rgb), 0.16);
border-radius: 4px;
font-size: 16px;
&:active,
&:focus {
border: 2px solid var(--button-bg);
}
}
.Roles-body {
.PreparingWorkspacePageBody {
max-width: 600px;
}
.Roles-list {
display: flex;
flex-wrap: wrap;
}
.role-button {
@include secondary-button;
@include button-large;
border-color: rgba(63, 67, 80, 0.16);
margin-right: $btn-margin-right;
margin-bottom: 24px;
color: #3f4350;
&.active {
background: rgba(var(--denim-button-bg-rgb), 0.16);
}
}
}
@include simple-in-and-out("Roles");

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

@@ -1,158 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {CSSTransition} from 'react-transition-group';
import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps';
import Title from './title';
import Description from './description';
import PageBody from './page_body';
import SingleColumnLayout from './single_column_layout';
import PageLine from './page_line';
import './roles.scss';
import {useDispatch, useSelector} from 'react-redux';
import {getWorkTemplateCategories as fetchCategories} from 'mattermost-redux/actions/work_templates';
import {getWorkTemplateCategories} from 'selectors/work_template';
import classNames from 'classnames';
import QuickInput from 'components/quick_input';
import {CategoryOther} from '@mattermost/types/work_templates';
type Props = PreparingWorkspacePageProps & {
role: Form['role'];
roleOther: Form['roleOther'];
setRole: (role: string, roleOther: string) => void;
className?: string;
quickNext(role: string): void;
}
const Roles = ({role, next, ...props}: Props) => {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const categories = useSelector(getWorkTemplateCategories);
let className = 'Roles-body';
useEffect(() => {
dispatch(fetchCategories());
}, []);
useEffect(() => {
if (props.show) {
props.onPageView();
}
}, [props.show]);
if (props.className) {
className += ' ' + props.className;
}
const title = (
<FormattedMessage
id={'onboarding_wizard.roles.title'}
defaultMessage='What is your primary function?'
/>
);
const description = (
<FormattedMessage
id={'onboarding_wizard.roles.description'}
defaultMessage={'Well use this to suggest templates to help get you started.'}
/>
);
const selectRole = (role: string) => {
if (role !== CategoryOther) {
props.quickNext(role);
}
props.setRole(role, '');
};
const roleIsSet = Boolean(role);
const roleOtherIsSet = Boolean(props.roleOther);
const canContinue = (roleIsSet && role !== CategoryOther) || (role === CategoryOther && roleOtherIsSet);
return (
<CSSTransition
in={props.show}
timeout={Animations.PAGE_SLIDE}
classNames={mapAnimationReasonToClass('Roles', props.transitionDirection)}
mountOnEnter={true}
unmountOnExit={true}
>
<div className={className}>
<SingleColumnLayout>
<PageLine
style={{
marginBottom: '50px',
marginLeft: '50px',
height: 'calc(25vh)',
}}
noLeft={true}
/>
{props.previous}
<Title>
{title}
</Title>
<Description>{description}</Description>
<PageBody>
<div className='Roles-list'>
{categories.map((category) => (
<button
key={category.id}
className={classNames('role-button', {active: category.id === role})}
onClick={() => selectRole(category.id)}
>
{category.name}
</button>
))}
</div>
{role === CategoryOther && (
<QuickInput
value={props.roleOther || ''}
className='Roles__input'
onChange={(e) => props.setRole(CategoryOther, e.target.value)}
placeholder={formatMessage({
id: 'onboarding_wizard.roles.other_input_placeholder',
defaultMessage: 'Please share your primary function',
})}
/>
)}
</PageBody>
<div>
<button
className='primary-button'
onClick={next}
disabled={!canContinue}
>
<FormattedMessage
id={'onboarding_wizard.next'}
defaultMessage='Continue'
/>
</button>
<button
className='link-style plugins-skip-btn'
onClick={props.skip}
>
<FormattedMessage
id={'onboarding_wizard.skip-button'}
defaultMessage='Skip'
/>
</button>
</div>
<PageLine
style={{
marginTop: '50px',
marginLeft: '50px',
height: 'calc(30vh)',
}}
noLeft={true}
/>
</SingleColumnLayout>
</div>
</CSSTransition>
);
};
export default Roles;

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@@ -5,7 +5,6 @@ import deepFreeze from 'mattermost-redux/utils/deep_freeze';
export const WizardSteps = {
Organization: 'Organization',
Roles: 'Roles',
Plugins: 'Plugins',
InviteMembers: 'InviteMembers',
LaunchingWorkspace: 'LaunchingWorkspace',
@@ -25,8 +24,6 @@ export function mapStepToNextName(step: WizardStep): string {
switch (step) {
case WizardSteps.Organization:
return 'admin_onboarding_next_organization';
case WizardSteps.Roles:
return 'admin_onboarding_next_roles';
case WizardSteps.Plugins:
return 'admin_onboarding_next_plugins';
case WizardSteps.InviteMembers:
@@ -42,8 +39,6 @@ export function mapStepToPrevious(step: WizardStep): string {
switch (step) {
case WizardSteps.Organization:
return 'admin_onboarding_previous_organization';
case WizardSteps.Roles:
return 'admin_onboarding_previous_roles';
case WizardSteps.Plugins:
return 'admin_onboarding_previous_plugins';
case WizardSteps.InviteMembers:
@@ -59,8 +54,6 @@ export function mapStepToPageView(step: WizardStep): string {
switch (step) {
case WizardSteps.Organization:
return 'pageview_admin_onboarding_organization';
case WizardSteps.Roles:
return 'pageview_admin_onboarding_roles';
case WizardSteps.Plugins:
return 'pageview_admin_onboarding_plugins';
case WizardSteps.InviteMembers:
@@ -76,8 +69,6 @@ export function mapStepToSubmitFail(step: WizardStep): string {
switch (step) {
case WizardSteps.Organization:
return 'admin_onboarding_organization_submit_fail';
case WizardSteps.Roles:
return 'admin_onboarding_roles_submit_fail';
case WizardSteps.Plugins:
return 'admin_onboarding_plugins_submit_fail';
case WizardSteps.InviteMembers:
@@ -93,8 +84,6 @@ export function mapStepToSkipName(step: WizardStep): string {
switch (step) {
case WizardSteps.Organization:
return 'admin_onboarding_skip_organization';
case WizardSteps.Roles:
return 'admin_onboarding_skip_roles';
case WizardSteps.Plugins:
return 'admin_onboarding_skip_plugins';
case WizardSteps.InviteMembers:
@@ -138,8 +127,6 @@ export const PLUGIN_NAME_TO_ID_MAP: PluginNameMap = {
export type Form = {
organization?: string;
role?: string;
roleOther?: string;
url?: string;
urlSkipped: boolean;
inferredProtocol: 'http' | 'https' | null;
@@ -197,8 +184,6 @@ export const emptyForm = deepFreeze({
invites: [],
skipped: false,
},
role: '',
roleOther: '',
});
export type PreparingWorkspacePageProps = {

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

@@ -17,7 +17,6 @@ exports[`components/sidebar should match snapshot 1`] = `
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
showWorkTemplateButton={true}
unreadFilterEnabled={false}
userGroupsEnabled={false}
/>
@@ -72,7 +71,6 @@ exports[`components/sidebar should match snapshot when direct channels modal is
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
showWorkTemplateButton={true}
unreadFilterEnabled={false}
userGroupsEnabled={false}
/>
@@ -131,7 +129,6 @@ exports[`components/sidebar should match snapshot when more channels modal is op
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
showWorkTemplateButton={true}
unreadFilterEnabled={false}
userGroupsEnabled={false}
/>

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

@@ -11,8 +11,6 @@ import Menu from 'components/widgets/menu/menu';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import {CreateAndJoinChannelsTour, InvitePeopleTour} from 'components/tours/onboarding_tour';
import {ModalIdentifiers} from 'utils/constants';
import WorkTemplateModal from 'components/work_templates';
type Props = {
canCreateChannel: boolean;
@@ -30,7 +28,6 @@ type Props = {
isAddChannelOpen: boolean;
openAddChannelOpen: (open: boolean) => void;
canCreateCustomGroups: boolean;
showWorkTemplateButton: boolean;
};
const AddChannelDropdown = ({
@@ -48,7 +45,6 @@ const AddChannelDropdown = ({
isAddChannelOpen,
openAddChannelOpen,
canCreateCustomGroups,
showWorkTemplateButton,
}: Props) => {
const intl = useIntl();
@@ -66,21 +62,6 @@ const AddChannelDropdown = ({
</Menu.Group>
);
let workTemplate;
if (showWorkTemplateButton) {
workTemplate = (
<Menu.ItemToggleModalRedux
id='work-template'
modalId={ModalIdentifiers.WORK_TEMPLATE}
dialogType={WorkTemplateModal}
text={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.work_template', defaultMessage: 'Create from a template'})}
extraText={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.work_template_extra', defaultMessage: 'Link channels, boards, and playbooks together'})}
icon={<i className='icon-layers-outline'/>}
className='work-template'
/>
);
}
let joinPublicChannel;
if (canJoinPublicChannel) {
joinPublicChannel = (
@@ -142,7 +123,6 @@ const AddChannelDropdown = ({
return (
<>
<Menu.Group>
{workTemplate}
{createChannel}
{joinPublicChannel}
{createDirectMessage}

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

@@ -22,7 +22,6 @@ import {getIsLhsOpen} from 'selectors/lhs';
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
import {getIsMobileView} from 'selectors/views/browser';
import {isModalOpen} from 'selectors/views/modals';
import {areWorkTemplatesEnabled} from 'selectors/work_template';
import {ModalIdentifiers} from 'utils/constants';
import Sidebar from './sidebar';
@@ -44,8 +43,6 @@ function mapStateToProps(state: GlobalState) {
const canCreateCustomGroups = haveISystemPermission(state, {permission: Permissions.CREATE_CUSTOM_GROUP}) && isCustomGroupsEnabled(state);
const showWorkTemplateButton = areWorkTemplatesEnabled(state);
return {
teamId: currentTeam ? currentTeam.id : '',
canCreatePrivateChannel,
@@ -66,7 +63,6 @@ function mapStateToProps(state: GlobalState) {
canCreateCustomGroups,
rhsState: getRhsState(state),
rhsOpen: getIsRhsOpen(state),
showWorkTemplateButton,
};
}

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

@@ -21,7 +21,6 @@ describe('components/sidebar', () => {
isKeyBoardShortcutModalOpen: false,
userGroupsEnabled: false,
canCreateCustomGroups: true,
showWorkTemplateButton: true,
actions: {
createCategory: jest.fn(),
fetchMyCategories: jest.fn(),

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

@@ -53,7 +53,6 @@ type Props = {
canCreateCustomGroups: boolean;
rhsState?: RhsState;
rhsOpen?: boolean;
showWorkTemplateButton: boolean;
};
type State = {
@@ -256,7 +255,6 @@ export default class Sidebar extends React.PureComponent<Props, State> {
unreadFilterEnabled={this.props.unreadFilterEnabled}
userGroupsEnabled={this.props.userGroupsEnabled}
canCreateCustomGroups={this.props.canCreateCustomGroups}
showWorkTemplateButton={this.props.showWorkTemplateButton}
/>
)}
<div

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

@@ -37,7 +37,6 @@ describe('Components/SidebarHeader', () => {
showCreateUserGroupModal: jest.fn(),
userGroupsEnabled: false,
canCreateCustomGroups: true,
showWorkTemplateButton: true,
};
mockState = {

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

@@ -98,7 +98,6 @@ export type Props = {
unreadFilterEnabled: boolean;
userGroupsEnabled: boolean;
canCreateCustomGroups: boolean;
showWorkTemplateButton: boolean;
}
const SidebarHeader: React.FC<Props> = (props: Props): JSX.Element => {
@@ -164,7 +163,6 @@ const SidebarHeader: React.FC<Props> = (props: Props): JSX.Element => {
canCreateCustomGroups={props.canCreateCustomGroups}
showCreateUserGroupModal={props.showCreateUserGroupModal}
userGroupsEnabled={props.userGroupsEnabled}
showWorkTemplateButton={props.showWorkTemplateButton}
/>
</SidebarHeaderContainer>
</CompassThemeProvider>

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

@@ -17,8 +17,6 @@ export const ChannelsTour = 'channels_tour';
export const OtherToolsTour = 'other_tools_tour';
export const WorkTemplatesTour = 'work_templates_tour';
export const TutorialTourName = {
ONBOARDING_TUTORIAL_STEP: 'tutorial_step',
ONBOARDING_TUTORIAL_STEP_FOR_GUESTS: 'tutorial_step_for_guest',
@@ -26,7 +24,6 @@ export const TutorialTourName = {
CRT_THREAD_PANE_STEP: 'crt_thread_pane_step',
AUTO_TOUR_STATUS: 'auto_tour_status',
EXPLORE_OTHER_TOOLS: 'explore_tools',
WORK_TEMPLATE_TUTORIAL: 'work_template',
};
export const OnboardingTourSteps = {
@@ -57,12 +54,6 @@ export const CrtTutorialSteps = {
FINISHED,
};
export const WorkTemplateTourSteps = {
PLAYBOOKS_TOUR_TIP: 0,
BOARDS_TOUR_TIP: 1,
FINISHED,
};
export const CrtTutorialTriggerSteps = {
START: 0,
STARTED: 1,
@@ -74,7 +65,6 @@ export const TTNameMapToATStatusKey = {
[TutorialTourName.CRT_TUTORIAL_STEP]: 'crt_tutorial_auto_tour_status',
[TutorialTourName.CRT_THREAD_PANE_STEP]: TutorialTourName.CRT_THREAD_PANE_STEP + AutoStatusSuffix,
[TutorialTourName.EXPLORE_OTHER_TOOLS]: TutorialTourName.EXPLORE_OTHER_TOOLS + AutoStatusSuffix,
[TutorialTourName.WORK_TEMPLATE_TUTORIAL]: TutorialTourName.WORK_TEMPLATE_TUTORIAL + AutoStatusSuffix,
};
export const TTNameMapToTourSteps = {
@@ -82,5 +72,4 @@ export const TTNameMapToTourSteps = {
[TutorialTourName.ONBOARDING_TUTORIAL_STEP_FOR_GUESTS]: OnboardingTourStepsForGuestUsers,
[TutorialTourName.CRT_TUTORIAL_STEP]: CrtTutorialSteps,
[TutorialTourName.EXPLORE_OTHER_TOOLS]: ExploreOtherToolsTourSteps,
[TutorialTourName.WORK_TEMPLATE_TUTORIAL]: WorkTemplateTourSteps,
};

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

@@ -5,23 +5,16 @@ import {useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {without} from 'lodash';
import {getCurrentUserId, isCurrentUserGuestUser} from 'mattermost-redux/selectors/entities/users';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {getWorkTemplatesLinkedProducts} from 'mattermost-redux/selectors/entities/general';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {close as closeLhs, open as openLhs} from 'actions/views/lhs';
import {setAddChannelDropdown} from 'actions/views/add_channel_dropdown';
import {switchToChannels} from 'actions/views/onboarding_tasks';
import {showRHSPlugin} from 'actions/views/rhs';
import {setProductMenuSwitcherOpen} from 'actions/views/product_menu';
import {useGetRHSPluggablesIds} from 'components/work_templates/hooks';
import {getHistory} from 'utils/browser_history';
import {suitePluginIds} from 'utils/constants';
import {GlobalState} from 'types/store';
import {useGetPluginsActivationState} from 'plugins/useGetPluginsActivationState';
@@ -34,13 +27,11 @@ import {
OnboardingTourSteps,
TTNameMapToTourSteps,
TutorialTourName,
WorkTemplateTourSteps,
} from './constant';
export const useGetTourSteps = (tourCategory: string) => {
const isGuestUser = useSelector((state: GlobalState) => isCurrentUserGuestUser(state));
const workTemplatesLinkedItems = useSelector(getWorkTemplatesLinkedProducts);
let tourSteps: Record<string, number> = TTNameMapToTourSteps[tourCategory];
const {playbooksPlugin, playbooksProductEnabled, boardsPlugin, boardsProductEnabled} = useGetPluginsActivationState();
@@ -55,17 +46,6 @@ export const useGetTourSteps = (tourCategory: string) => {
delete steps.BOARDS_TOUR;
}
tourSteps = steps;
} else if (tourCategory === TutorialTourName.WORK_TEMPLATE_TUTORIAL) {
const steps: Record<string, number> = tourSteps as typeof WorkTemplateTourSteps;
if (workTemplatesLinkedItems.playbooks && workTemplatesLinkedItems.playbooks === 0) {
delete steps.PLAYBOOKS_TOUR;
}
if (workTemplatesLinkedItems.boards && workTemplatesLinkedItems.boards === 0) {
delete steps.BOARDS_TOUR;
}
tourSteps = steps;
} else if (tourCategory === TutorialTourName.ONBOARDING_TUTORIAL_STEP && isGuestUser) {
// restrict the 'learn more about messaging' tour when user is guest (townSquare, channel creation and user invite are restricted to guests)
tourSteps = TTNameMapToTourSteps[TutorialTourName.ONBOARDING_TUTORIAL_STEP_FOR_GUESTS];
@@ -76,12 +56,6 @@ export const useHandleNavigationAndExtraActions = (tourCategory: string) => {
const dispatch = useDispatch();
const currentUserId = useSelector(getCurrentUserId);
const teamUrl = useSelector((state: GlobalState) => getCurrentRelativeTeamUrl(state));
const {pluggableId, rhsPluggableIds} = useGetRHSPluggablesIds();
const pluggableIds = [rhsPluggableIds.get(suitePluginIds.boards), rhsPluggableIds.get(suitePluginIds.playbooks)];
const channelLinkedItems = useSelector(getWorkTemplatesLinkedProducts);
const boardsCount = channelLinkedItems?.boards || 0;
const playbooksCount = channelLinkedItems?.playbooks || 0;
const nextStepActions = useCallback((step: number) => {
if (tourCategory === TutorialTourName.ONBOARDING_TUTORIAL_STEP) {
@@ -165,26 +139,6 @@ export const useHandleNavigationAndExtraActions = (tourCategory: string) => {
}
default:
}
} else if (tourCategory === TutorialTourName.WORK_TEMPLATE_TUTORIAL) {
const navigationPluggableId = without(pluggableIds, pluggableId)[0];
const stepMatches = step === WorkTemplateTourSteps.BOARDS_TOUR_TIP || step === WorkTemplateTourSteps.PLAYBOOKS_TOUR_TIP;
const multiStep = Boolean(boardsCount && playbooksCount);
if (!multiStep) {
const preferences = [
{
user_id: currentUserId,
category: TutorialTourName.WORK_TEMPLATE_TUTORIAL,
name: currentUserId,
value: FINISHED.toString(),
},
];
dispatch(savePreferences(currentUserId, preferences));
return;
}
if (navigationPluggableId && stepMatches) {
dispatch(showRHSPlugin(navigationPluggableId));
}
}
}, [currentUserId, teamUrl, tourCategory]);

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

@@ -1,67 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useFollowElementDimensions, useMeasurePunchouts} from '@mattermost/components';
import OnboardingWorkTemplateTourTip from './worktemplate_explore_tour_tip';
import {useShowTourTip} from './useShowTourTip';
export const BoardsTourTip = (): JSX.Element | null => {
const {formatMessage} = useIntl();
const {playbooksCount, boardsCount, showBoardsTour} = useShowTourTip();
const dimensions = useFollowElementDimensions('sidebar-right');
const overlayPunchOut = useMeasurePunchouts(['sidebar-right'], [dimensions?.width]);
if (!showBoardsTour) {
return null;
}
const title = (
<FormattedMessage
id='pluggable_rhs.tourtip.boards.title'
defaultMessage={'Access your {count} linked {count, plural, one {board} other {boards}}!'}
values={{count: boardsCount === 0 ? undefined : String(boardsCount)}}
/>
);
const screen = (
<ul>
<li>
{formatMessage({
id: 'pluggable_rhs.tourtip.boards.access',
defaultMessage: 'Access your linked boards from the Boards icon on the right hand App bar.',
})}
</li>
<li>
{formatMessage({
id: 'pluggable_rhs.tourtip.boards.click',
defaultMessage: 'Click into boards from this right panel.',
})}
</li>
<li>
{formatMessage({
id: 'pluggable_rhs.tourtip.boards.review',
defaultMessage: 'Review boards updates from your channels.',
})}
</li>
</ul>
);
return (
<OnboardingWorkTemplateTourTip
pulsatingDotPlacement={'left'}
pulsatingDotTranslate={{x: 10, y: -350}}
title={title}
screen={screen}
overlayPunchOut={overlayPunchOut}
singleTip={playbooksCount === 0}
placement='left-start'
showOptOut={false}
interactivePunchOut={true}
/>
);
};

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

@@ -1,5 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export * from './boards_tour_tip';
export * from './playbooks_tour_tip';

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

@@ -1,67 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useFollowElementDimensions, useMeasurePunchouts} from '@mattermost/components';
import {useShowTourTip} from './useShowTourTip';
import OnboardingWorkTemplateTourTip from './worktemplate_explore_tour_tip';
export const PlaybooksTourTip = (): JSX.Element | null => {
const {formatMessage} = useIntl();
const {playbooksCount, boardsCount, showPlaybooksTour} = useShowTourTip();
const dimensions = useFollowElementDimensions('sidebar-right');
const overlayPunchOut = useMeasurePunchouts(['sidebar-right'], [dimensions?.width]);
if (!showPlaybooksTour) {
return null;
}
const title = (
<FormattedMessage
id='pluggable_rhs.tourtip.playbooks.title'
defaultMessage={'Access your {count} linked {count, plural, one {playbook} other {playbooks}}!'}
values={{count: playbooksCount === 0 ? undefined : String(playbooksCount)}}
/>
);
const screen = (
<ul>
<li>
{formatMessage({
id: 'pluggable_rhs.tourtip.playbooks.access',
defaultMessage: 'Access your linked playbooks from the Playbooks icon on the right hand App bar.',
})}
</li>
<li>
{formatMessage({
id: 'pluggable_rhs.tourtip.playbooks.click',
defaultMessage: 'Click into playbooks from this right panel.',
})}
</li>
<li>
{formatMessage({
id: 'pluggable_rhs.tourtip.playbooks.review',
defaultMessage: 'Review playbook updates from your channels.',
})}
</li>
</ul>
);
return (
<OnboardingWorkTemplateTourTip
pulsatingDotPlacement={'left'}
pulsatingDotTranslate={{x: 10, y: -140}}
title={title}
screen={screen}
singleTip={boardsCount === 0}
overlayPunchOut={overlayPunchOut}
placement='left-start'
showOptOut={false}
interactivePunchOut={true}
/>
);
};

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

@@ -1,47 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useSelector} from 'react-redux';
import {getCurrentChannelId, getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {getConfig, getWorkTemplatesLinkedProducts} from 'mattermost-redux/selectors/entities/general';
import {getInt} from 'mattermost-redux/selectors/entities/preferences';
import {getActiveRhsComponent} from 'selectors/rhs';
import {suitePluginIds} from 'utils/constants';
import {TutorialTourName, WorkTemplateTourSteps} from '../constant';
import {GlobalState} from 'types/store';
export const useShowTourTip = () => {
const activeRhsComponent = useSelector(getActiveRhsComponent);
const pluginId = activeRhsComponent?.pluginId || '';
const currentChannelId = useSelector(getCurrentChannelId);
const currentUserId = useSelector(getCurrentUserId);
const enableTutorial = useSelector(getConfig).EnableTutorial === 'true';
const tutorialStep = useSelector((state: GlobalState) => getInt(state, TutorialTourName.WORK_TEMPLATE_TUTORIAL, currentUserId, 0));
const workTemplateTourTipShown = tutorialStep === WorkTemplateTourSteps.FINISHED;
const channelLinkedItems = useSelector(getWorkTemplatesLinkedProducts);
const boardsCount = channelLinkedItems?.boards || 0;
const playbooksCount = channelLinkedItems?.playbooks || 0;
const channelId = channelLinkedItems?.channelId || null;
const showProductTour = channelId && channelId === currentChannelId && !workTemplateTourTipShown && enableTutorial;
const showBoardsTour = showProductTour && pluginId === suitePluginIds.boards && boardsCount > 0;
const showPlaybooksTour = showProductTour && pluginId === suitePluginIds.playbooks && playbooksCount > 0;
return {
showBoardsTour,
showPlaybooksTour,
boardsCount,
playbooksCount,
showProductTour,
};
};

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

@@ -1,17 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {ChannelsTourTip, ChannelsTourTipProps, TutorialTourName} from 'components/tours';
const OnboardingWorkTemplateTourTip = (props: Omit<ChannelsTourTipProps, 'tourCategory'>) => {
return (
<ChannelsTourTip
{...props}
tourCategory={TutorialTourName.WORK_TEMPLATE_TUTORIAL}
/>
);
};
export default OnboardingWorkTemplateTourTip;

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

@@ -1,186 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import styled from 'styled-components';
import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import PublicPrivateSelector from 'components/widgets/public-private-selector/public-private-selector';
import {trackEvent} from 'actions/telemetry_actions';
import Constants, {TELEMETRY_CATEGORIES} from 'utils/constants';
import {isEnterpriseOrE20License} from 'utils/license_utils';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
import {Permissions} from 'mattermost-redux/constants';
import {GlobalState} from 'types/store';
import {Visibility, WorkTemplate} from '@mattermost/types/work_templates';
import {ChannelType} from '@mattermost/types/channels';
export interface CustomizeProps {
className?: string;
name: string;
visibility: Visibility;
template: WorkTemplate;
onNameChanged: (name: string) => void;
onVisibilityChanged: (visibility: Visibility) => void;
}
const Customize = ({
name,
visibility,
template,
onNameChanged,
onVisibilityChanged,
...props
}: CustomizeProps) => {
const {formatMessage} = useIntl();
const license = useSelector(getLicense);
const licenseIsEnterprise = isEnterpriseOrE20License(license);
const templateHasChannels = template.content.findIndex((item) => item.channel) !== -1;
const templateHasBoards = template.content.findIndex((item) => item.board) !== -1;
const templateHasPlaybooks = template.content.findIndex((item) => item.playbook) !== -1;
const canCreatePublicChannel = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.CREATE_PUBLIC_CHANNEL));
const canCreatePrivateChannel = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.CREATE_PRIVATE_CHANNEL));
const canCreatePublicPlaybook = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.PLAYBOOK_PUBLIC_CREATE));
const canCreatePrivatePlaybook = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.PLAYBOOK_PRIVATE_CREATE));
useEffect(() => {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'pageview_customize');
}, []);
const privacySelectorValue = (visibility === Visibility.Public ? Constants.OPEN_CHANNEL : Constants.PRIVATE_CHANNEL) as ChannelType;
const onPrivacySelectorChanged = (value: ChannelType) => {
onVisibilityChanged(value === Constants.PRIVATE_CHANNEL ? Visibility.Private : Visibility.Public);
};
let privateButtonProps = {};
let publicButtonProps = {};
if (templateHasPlaybooks) {
if (!canCreatePublicPlaybook) {
publicButtonProps = {
tooltip: formatMessage({id: 'work_templates.customize.public_playbook_permission_issue', defaultMessage: 'You do not have permission to create public playbooks.'}),
disabled: true,
};
}
if (!canCreatePrivatePlaybook) {
privateButtonProps = {
tooltip: formatMessage({id: 'work_templates.customize.private_playbook_permission_issue', defaultMessage: 'You do not have permission to create private playbooks.'}),
disabled: true,
};
}
}
if (templateHasChannels) {
if (!canCreatePublicChannel) {
publicButtonProps = {
tooltip: formatMessage({id: 'work_templates.customize.public_channel_permission_issue', defaultMessage: 'You do not have permission to create public channels.'}),
disabled: true,
};
}
if (!canCreatePrivateChannel) {
privateButtonProps = {
tooltip: formatMessage({id: 'work_templates.customize.private_channel_permission_issue', defaultMessage: 'You do not have permission to create private channels.'}),
disabled: true,
};
}
}
// leave this rule last as it has priority
if (templateHasPlaybooks && !licenseIsEnterprise) {
privateButtonProps = {
tooltip: formatMessage({id: 'work_templates.customize.private_playbook_license_issue', defaultMessage: 'Private playbooks requires an Enterprise license.'}),
locked: true,
};
}
let nameFieldLabel;
if (templateHasChannels && templateHasBoards && templateHasPlaybooks) {
nameFieldLabel = formatMessage({id: 'work_templates.customize.name_label_all', defaultMessage: 'Name your channel, board, and playbook'});
} else if (templateHasChannels && templateHasBoards) {
nameFieldLabel = formatMessage({id: 'work_templates.customize.name_label_channels_boards', defaultMessage: 'Name your channel and board'});
} else if (templateHasChannels && templateHasPlaybooks) {
nameFieldLabel = formatMessage({id: 'work_templates.customize.name_label_channels_playbooks', defaultMessage: 'Name your channel and playbook'});
}
return (
<div className={props.className}>
<div className='name-section-container'>
<p>
<strong>
{nameFieldLabel}
</strong>
</p>
<p className='customize-name-text'>
{formatMessage({id: 'work_templates.customize.name_description', defaultMessage: 'This will help you and others find your project items. You can always edit this later.'})}
</p>
<input
type='text'
autoFocus={true}
placeholder={formatMessage({id: 'work_templates.customize.name_input_placeholder', defaultMessage: 'e.g. Web app, Growth, Customer Journey etc.'})}
value={name}
onChange={(e) => onNameChanged(e.target.value)}
maxLength={Constants.MAX_CHANNELNAME_LENGTH}
/>
</div>
<div className='visibility-section-container'>
<p>
<strong>
{formatMessage({id: 'work_templates.customize.visibility_title', defaultMessage: 'Who should have access to this?'})}
</strong>
</p>
<PublicPrivateSelector
selected={privacySelectorValue}
onChange={onPrivacySelectorChanged}
privateButtonProps={privateButtonProps}
publicButtonProps={publicButtonProps}
/>
</div>
</div>
);
};
const StyledCustomized = styled(Customize)`
display: flex;
flex-direction: column;
width: 509px;
margin: 0 auto;
.public-private-selector .public-private-selector-button.locked {
opacity: 1;
}
strong {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: var(--center-channel-text);
}
input {
padding: 10px 16px;
font-size: 14px;
width: 100%;
border-radius: 4px;
border: 1px solid rgba(var(--center-channel-text-rgb), 0.16);
&:focus {
border: 1px solid var(--button-bg);
box-shadow: inset 0 0 0 1px var(--button-bg);
}
}
.name-section-container {
margin-top: 33px;
}
.visibility-section-container {
margin-top: 56px;
}
.customize-name-text {
font-size: 12px;
}
`;
export default StyledCustomized;

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

@@ -1,131 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import classNames from 'classnames';
import {useIntl} from 'react-intl';
import styled from 'styled-components';
import {trackEvent} from 'actions/telemetry_actions';
import {Category, WorkTemplate} from '@mattermost/types/work_templates';
import {TELEMETRY_CATEGORIES} from 'utils/constants';
import UseCaseMenuItem from './menu/use_case';
const Categories = styled.div`
h2 {
margin: 0;
padding: 8px 16px;
font-weight: 600;
font-size: 12px;
line-height: 16px;
letter-spacing: 0.02em;
text-transform: uppercase;
}
ul {
list-style: none;
padding: 0;
width: 176px;
}
`;
const CategoryButton = styled.button`
width: 155px;
padding: 10px 16px;
border: 0;
background: none;
text-align: left;
&:hover {
background: rgba(var(--denim-button-bg-rgb), 0.04);
cursor: pointer;
}
&.selected {
background: rgba(var(--denim-button-bg-rgb), 0.04);
font-weight: 600;
color: var(--denim-button-bg);
cursor: pointer;
}
`;
const UseCases = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
width: 692px;
`;
interface MenuProps {
className?: string;
onTemplateSelected: (template: WorkTemplate, quickUse: boolean) => void;
categories: Category[];
changeCategory: (category: Category) => void;
workTemplates: Record<string, WorkTemplate[]>;
currentCategoryId: string;
disableQuickUse: boolean;
}
const Menu = ({className, disableQuickUse, categories, workTemplates, currentCategoryId, changeCategory, onTemplateSelected}: MenuProps) => {
const {formatMessage} = useIntl();
useEffect(() => {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'pageview_menu');
}, []);
const quickUse = (template: WorkTemplate) => {
onTemplateSelected(template, true);
};
const selectTemplate = (template: WorkTemplate) => {
onTemplateSelected(template, false);
};
if (!categories.length) {
return null;
}
return (
<div className={className}>
<Categories>
<h2>
{formatMessage({id: 'work_templates.menu.template_title', defaultMessage: 'TEMPLATE'})}
</h2>
<ul>
{categories.map((category) => (
<li key={category.id}>
<CategoryButton
onClick={() => changeCategory(category)}
className={classNames({selected: category.id === currentCategoryId})}
>
{category.name}
</CategoryButton>
</li>
))}
</ul>
</Categories>
<UseCases>
{workTemplates[currentCategoryId]?.map((workTemplate) => (
<UseCaseMenuItem
disableQuickUse={disableQuickUse}
key={workTemplate.id}
name={workTemplate.useCase}
illustration={workTemplate.illustration}
channelsCount={workTemplate.content.filter((c) => c.channel).length}
boardsCount={workTemplate.content.filter((c) => c.board).length}
playbooksCount={workTemplate.content.filter((c) => c.playbook).length}
onQuickUse={() => quickUse(workTemplate)}
onSelectTemplate={() => selectTemplate(workTemplate)}
/>
))}
</UseCases>
</div>
);
};
const StyledMenu = styled(Menu)`
display: flex;
`;
export default StyledMenu;

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

@@ -1,192 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {useIntl} from 'react-intl';
import styled from 'styled-components';
const QuickUse = styled.button`
position: absolute;
top: 7px;
right: 7px;
text-align: center;
padding: 4px 10px;
border: 0px;
background: var(--denim-button-bg);
border-radius: 4px;
font-weight: 600;
font-size: 11px;
line-height: 16px;
color: var(--button-color);
visibility: hidden;
opacity: 0;
z-index: 2;
transition: visibility 0.2s ease-in-out, opacity 0.2s ease-in-out;
`;
interface UseCaseProps {
className?: string;
name: string;
illustration: string;
channelsCount: number;
boardsCount: number;
playbooksCount: number;
disableQuickUse: boolean;
onQuickUse: () => void;
onSelectTemplate: () => void;
}
const UseCase = (props: UseCaseProps) => {
const {formatMessage, formatList} = useIntl();
const details = useMemo(() => {
const detailBuilder: string[] = [];
if (props.channelsCount) {
detailBuilder.push(formatMessage({
id: 'work_templates.menu.usecase_channels_count',
defaultMessage: '{channelsCount, plural, =1 {# channel} other {# channels}}',
}, {channelsCount: props.channelsCount}));
}
if (props.boardsCount) {
detailBuilder.push(formatMessage({
id: 'work_templates.menu.usecase_boards_count',
defaultMessage: '{boardsCount, plural, =1 {# board} other {# boards}}',
}, {boardsCount: props.boardsCount}));
}
if (props.playbooksCount) {
detailBuilder.push(formatMessage({
id: 'work_templates.menu.usecase_playbooks_count',
defaultMessage: '{playbooksCount, plural, =1 {# playbook} other {# playbooks}}',
}, {playbooksCount: props.playbooksCount}));
}
return formatList(detailBuilder, {style: 'narrow'});
}, [props.channelsCount, props.boardsCount, props.playbooksCount]);
const selectTemplate = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
props.onSelectTemplate();
};
const quickUse = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
props.onQuickUse();
};
return (
<div
className={props.className}
onClick={selectTemplate}
>
<div className='illustration'>
<QuickUse
onClick={quickUse}
disabled={props.disableQuickUse}
>{formatMessage({id: 'work_templates.menu.quick_use', defaultMessage: 'Quick use'})}</QuickUse>
<img src={props.illustration}/>
</div>
<div className='name'>
{props.name}
<p className='details'>
{details}
</p>
</div>
</div>
);
};
const StyledUseCaseMenuItem = styled(UseCase)`
display: flex;
flex-direction: column;
width: 220px;
border: 1px solid rgba(var(--center-channel-text-rgb), 0.16);
border-radius: 8px;
cursor: pointer;
margin-bottom: 16px;
margin-right: 10px;
.illustration {
height: 130px;
background: rgba(73, 146, 243, 0.2);
border-radius: 8px 8px 0px 0px;
display: flex;
align-items: flex-end;
justify-content: center;
position: relative;
flex-grow: 1;
overflow-x: hidden;
overflow-y: hidden;
transition: height 0.2s ease-in-out;
img {
width: 204px;
height: 123px;
z-index: 1;
transition: margin 0.2s ease-in-out;
}
}
.name {
padding: 14px 12px;
width: 220px;
height: 44px;
font-family: 'Open Sans';
line-height: 16px;
font-weight: 600;
font-size: 12px;
line-height: 16px;
color: var(--center-channel-color);
transition: height 0.2s ease-in-out;
flex-grow: 2;
.details {
visibility: hidden;
margin-bottom: 12px;
opacity: 0;
font-weight: 400;
font-size: 11px;
line-height: 16px;
letter-spacing: 0.02em;
color: rgba(var(--center-channel-text-rgb), 0.72);
transition: visibility 0.2s ease-in-out, opacity 0.2s ease-in-out;
}
}
&:hover {
box-shadow: var(--elevation-2);
${QuickUse} {
visibility: visible;
opacity: 1;
}
img {
margin-bottom: -12px;
}
.name {
height: 56px;
padding: 12px 12px 0;
.details {
visibility: visible;
opacity: 1;
}
}
.illustration {
height: 118px;
}
}
`;
export default StyledUseCaseMenuItem;

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

@@ -1,22 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import styled from 'styled-components';
import {GenericModal} from '@mattermost/components';
const Modal = styled(GenericModal)`
width: 960px;
.modal-body {
min-height: 450px;
}
&.work-template-modal--customize, &.work-template-modal--preview {
.modal-body {
background: rgba(var(--denim-button-bg-rgb), 0.04);
}
}
`;
export default Modal;

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

@@ -1,364 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import styled from 'styled-components';
import {CSSTransition} from 'react-transition-group';
import {useSelector} from 'react-redux';
import {AccordionItemType} from 'components/common/accordion/accordion';
import {trackEvent} from 'actions/telemetry_actions';
import {GlobalState} from 'types/store';
import {TELEMETRY_CATEGORIES} from 'utils/constants';
import {Board, Channel, Integration, Playbook, WorkTemplate} from '@mattermost/types/work_templates';
import {MarketplacePlugin} from '@mattermost/types/marketplace';
import {getTemplateDefaultIllustration} from '../utils';
import Accordion from './preview/accordion';
import Chip from './preview/chip';
import PreviewSection from './preview/section';
export interface PreviewProps {
className?: string;
template: WorkTemplate;
pluginsEnabled: boolean;
}
interface IllustrationAnimations {
prior: {
animateIn: boolean;
illustration: string;
};
current: {
animateIn: boolean;
illustration: string;
};
}
const ANIMATE_TIMEOUTS = {
appear: 0,
enter: 200,
exit: 200,
};
const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
const {formatMessage} = useIntl();
const nodeRefForPrior = useRef(null);
const nodeRefForCurrent = useRef(null);
const [integrations, setIntegrations] = useState<Integration[]>();
const marketplacePlugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins);
const loadedPlugins = useSelector((state: GlobalState) => state.plugins.plugins);
const [illustrationDetails, setIllustrationDetails] = useState<IllustrationAnimations>(() => {
const defaultIllustration = getTemplateDefaultIllustration(template);
return {
prior: {
animateIn: false,
illustration: defaultIllustration,
},
current: {
animateIn: true,
illustration: defaultIllustration,
},
};
});
useEffect(() => {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'pageview_preview');
}, []);
useEffect(() => {
if (illustrationDetails.prior.animateIn) {
setIllustrationDetails((prevState: IllustrationAnimations) => ({
prior: {
...prevState.prior,
animateIn: false,
},
current: {
...prevState.current,
animateIn: true,
},
}));
}
}, [illustrationDetails.prior.animateIn]);
const handleIllustrationUpdate = (illustration: string) => {
// don't refresh if this is the same illustration
if (illustrationDetails.current.illustration === illustration) {
return;
}
setIllustrationDetails({
prior: {...illustrationDetails.current},
current: {
animateIn: false,
illustration,
},
});
};
const [channels, boards, playbooks, availableIntegrations] = useMemo(() => {
const channels: Channel[] = [];
const boards: Board[] = [];
const playbooks: Playbook[] = [];
const availableIntegrations: Integration[] = [];
template.content.forEach((c) => {
if (c.channel) {
channels.push(c.channel);
}
if (c.board) {
boards.push(c.board);
}
if (c.playbook) {
playbooks.push(c.playbook);
}
if (c.integration && c.integration.recommended) {
availableIntegrations.push(c.integration);
}
});
return [channels, boards, playbooks, availableIntegrations];
}, [template.content]);
useEffect(() => {
if (!pluginsEnabled) {
return;
}
const intg =
availableIntegrations?.
flatMap((integration) => {
return marketplacePlugins.reduce((acc: Integration[], curr) => {
if (curr.manifest.id === integration.id) {
const installed = Boolean(loadedPlugins[integration.id]);
acc.push({
...integration,
name: curr.manifest.name,
icon: curr.icon_data,
installed,
});
return acc;
}
return acc;
}, [] as Integration[]);
}).sort((first: Integration) => {
return first.installed ? -1 : 1;
});
if (intg?.length) {
setIntegrations(intg);
}
}, [marketplacePlugins, availableIntegrations, loadedPlugins, pluginsEnabled]);
// building accordion items
const accordionItemsData: AccordionItemType[] = [];
if (channels.length > 0) {
accordionItemsData.push({
id: 'channels',
icon: <i className='icon-product-channels'/>,
title: formatMessage({id: 'work_templates.preview.accordion_title_channels', defaultMessage: 'Channels'}),
extraContent: <Chip>{channels.length}</Chip>,
items: [(
<PreviewSection
key={'channels'}
id={'channels'}
message={template.description.channel.message}
items={channels}
onUpdateIllustration={(illustration) => handleIllustrationUpdate(illustration)}
/>
)],
});
}
if (boards.length > 0) {
accordionItemsData.push({
id: 'boards',
icon: <i className='icon-product-boards'/>,
title: formatMessage({id: 'work_templates.preview.accordion_title_boards', defaultMessage: 'Boards'}),
extraContent: <Chip>{boards.length}</Chip>,
items: [(
<PreviewSection
key={'boards'}
id={'boards'}
message={template.description.board.message}
items={boards}
onUpdateIllustration={(illustration) => handleIllustrationUpdate(illustration)}
/>
)],
});
}
if (playbooks.length > 0) {
accordionItemsData.push({
id: 'playbooks',
icon: <i className='icon-product-playbooks'/>,
title: formatMessage({id: 'work_templates.preview.accordion_title_playbooks', defaultMessage: 'Playbooks'}),
extraContent: <Chip>{playbooks.length}</Chip>,
items: [(
<PreviewSection
key={'playbooks'}
id={'playbooks'}
message={template.description.playbook.message}
items={playbooks}
onUpdateIllustration={(illustration) => handleIllustrationUpdate(illustration)}
/>
)],
});
}
if (pluginsEnabled && integrations?.length) {
accordionItemsData.push({
id: 'integrations',
icon: <i className='icon-power-plug-outline'/>,
title: 'Integrations',
extraContent: <Chip>{integrations.length}</Chip>,
items: [(
<PreviewSection
key={'integrations'}
id={'integrations'}
message={template.description.integration.message}
items={integrations}
categoryId={template.category}
/>
)],
});
}
// When opening an accordion section, change the illustration to whatever has been open
const handleItemOpened = (index: number) => {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'expand_preview_section', {section: accordionItemsData[index].id, category: template.category, template: template.id});
const item = accordionItemsData[index];
const newPrior = {
...illustrationDetails.current,
animateIn: true,
};
const newCurrent: IllustrationAnimations['current'] = {
animateIn: false,
illustration: '',
};
switch (item.id) {
case 'channels':
newCurrent.illustration = channels[0].illustration;
break;
case 'boards':
newCurrent.illustration = boards[0].illustration;
break;
case 'playbooks':
newCurrent.illustration = playbooks[0].illustration;
break;
case 'integrations':
newCurrent.illustration = template.description.integration.illustration;
break;
default:
return;
}
setIllustrationDetails({
prior: newPrior,
current: newCurrent,
});
};
return (
<div className={className}>
<div className='content-side'>
<strong>{formatMessage({id: 'work_templates.preview.what_you_get', defaultMessage: 'Here\'s what you\'ll get:'})}</strong>
<Accordion
accordionItemsData={accordionItemsData}
openFirstElement={true}
onItemOpened={handleItemOpened}
/>
</div>
<div className='img-wrapper'>
<CSSTransition
nodeRef={nodeRefForPrior}
in={illustrationDetails.prior.animateIn}
timeout={ANIMATE_TIMEOUTS}
classNames='prior-illustration'
>
<img
ref={nodeRefForPrior}
src={illustrationDetails.prior.illustration}
/>
</CSSTransition>
<CSSTransition
nodeRef={nodeRefForCurrent}
in={illustrationDetails.current.animateIn}
timeout={ANIMATE_TIMEOUTS}
classNames='current-illustration'
>
<img
ref={nodeRefForCurrent}
src={illustrationDetails.current.illustration}
/>
</CSSTransition>
</div>
</div>
);
};
const StyledPreview = styled(Preview)`
display: flex;
.content-side {
min-width: 387px;
width: 387px;
height: 416px;
padding-right: 32px;
margin-top: 17px;
}
strong {
display: block;
font-family: 'Metropolis';
font-weight: 600;
font-size: 18px;
line-height: 24px;
color: var(--center-channel-text);
margin-bottom: 8px;
}
.img-wrapper {
position: relative;
width: 100%;
margin-top: 32px;
}
img {
box-shadow: var(--elevation-2);
border-radius: 8px;
position: absolute;
}
.prior-illustration-enter,
.prior-illustration-enter-done,
.prior-illustration-exit-done {
opacity: 0;
}
.prior-illustration-exit {
opacity: 1;
}
.prior-illustration-exit-active {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.current-illustration-enter,
.current-illustration-exit,
.current-illustration-exit-done {
opacity: 0;
}
.current-illustration-enter-active {
opacity: 1;
transition: opacity 200ms ease-in-out;
}
.current-illustration-enter-done {
opacity: 1;
}
`;
export default StyledPreview;

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

@@ -1,66 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import styled from 'styled-components';
import LibAccordion from 'components/common/accordion/accordion';
import Chip from './chip';
const Accordion = styled(LibAccordion)`
&.Accordion {
.accordion-card {
margin-bottom: 8px;
border-radius: 4px;
border: 1px solid transparent;
color: var(--center-channel-color);
.accordion-card-header {
padding: 14.5px 0px 14.5px 16px;
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: var(--center-channel-color);
align-items: center;
border-radius: 4px 4px 0 0;
&__extraContent {
margin-left: 2px;
}
&__chevron {
max-width: initial;
font-size: 18px;
line-height: 20px;
font-weight: 600;
}
}
.accordion-card-container__content {
padding: 4px 16px 16px 16px;
font-size: 12px;
line-height: 16px;
ul {
list-style: disc;
}
}
&.active {
border-color: var(--denim-button-bg);
.accordion-card-header {
color: var(--denim-button-bg);
padding-bottom: 4px;
}
${Chip} {
background: rgba(var(--denim-button-bg-rgb), 0.08);
color: var(--denim-button-bg);
}
}
}
}
`;
export default Accordion;

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

@@ -1,20 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import styled from 'styled-components';
const Chip = styled.span`
padding: 0px 4px;
width: 20px;
height: 16px;
border-radius: 8px;
font-weight: 700;
font-size: 11px;
line-height: 16px;
letter-spacing: 0.02em;
background: rgba(var(--center-channel-text-rgb), 0.08);
color: rgba(var(--center-channel-text-rgb), 0.56);
`;
export default Chip;

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

@@ -1,310 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactNode, useCallback, useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import classnames from 'classnames';
import styled from 'styled-components';
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
import store from 'stores/redux_store';
import NotifyAdminCTA from 'components/notify_admin_cta/notify_admin_cta';
import {MattermostFeatures} from 'utils/constants';
interface GenericPreviewSectionProps {
items: Array<{ id: string; name?: string; illustration?: string }>;
onUpdateIllustration?: (illustration: string) => void;
className?: string;
message?: string;
id?: string;
}
type IntegrationPreviewSectionItemsProps = {id: string; name?: string; category?: string; description?: string; icon?: string; installed?: boolean}
interface IntegrationPreviewSectionProps {
items: IntegrationPreviewSectionItemsProps[];
className?: string;
message?: string;
id?: string;
categoryId?: string;
}
const SYSCONSOLE_WRITE_PLUGINS = 'sysconsole_write_plugins';
const getState = store.getState;
const viewPreview = (props: GenericPreviewSectionProps | IntegrationPreviewSectionProps) => {
if (props.id === 'integrations') {
const integrationsProps = props as IntegrationPreviewSectionProps;
return (
<IntegrationsPreview
categoryId={integrationsProps.categoryId}
items={integrationsProps.items}
/>);
}
const genericProps = props as GenericPreviewSectionProps;
return (
<GenericPreview
items={props.items}
onUpdateIllustration={genericProps.onUpdateIllustration}
/>);
};
const PreviewSection = (props: GenericPreviewSectionProps | IntegrationPreviewSectionProps) => {
const {formatMessage} = useIntl();
return (
<div className={props.className}>
<p>
{props.message}
</p>
<span className='included-title'>
{
formatMessage({
id: 'work_templates.preview.section.included',
defaultMessage: 'Included',
})
}
</span>
{viewPreview(props)}
</div>
);
};
const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps) => {
const state = getState();
const {formatMessage} = useIntl();
const haveIWritePluginPermission = haveISystemPermission(state, {permission: SYSCONSOLE_WRITE_PLUGINS});
const [pluginInstallationPossible, setPluginInstallationPossible] = useState(false);
useEffect(() => {
if (haveIWritePluginPermission) {
setPluginInstallationPossible(true);
}
}, [haveIWritePluginPermission]);
const pluginsToInstall = items.filter((item) => !item.installed);
const createWarningMessage = () => {
if (pluginsToInstall.length === 1) {
return formatMessage(
{
id: 'work_templates.preview.integrations.admin_install.single_plugin',
defaultMessage: '{plugin} will not be added until admin installs it.',
},
{
plugin: pluginsToInstall[0].name,
});
} else if (pluginsToInstall.length > 1) {
return formatMessage({
id: 'work_templates.preview.integrations.admin_install.multiple_plugin',
defaultMessage: 'Integrations will not be added until admin installs them.',
});
}
return '';
};
const warningMessage = pluginInstallationPossible ? '' : createWarningMessage();
const notifyAdminCTA = formatMessage({
id: 'work_templates.preview.integrations.admin_install.notify',
defaultMessage: 'Notify admin to install integrations.',
});
const makeIntegrationSubtext = useCallback((integration: IntegrationPreviewSectionItemsProps) => {
if (integration.installed) {
return formatMessage({
id: 'work_templates.preview.integrations.already_installed',
defaultMessage: 'Already installed',
});
}
if (!pluginInstallationPossible) {
return formatMessage({
id: 'work_templates.preview.integrations.app_install',
defaultMessage: 'App Install',
});
}
return formatMessage({
id: 'work_templates.preview.integrations.to_be_installed',
defaultMessage: 'To be installed',
});
}, [pluginInstallationPossible, formatMessage]);
return (
<div className='preview-integrations'>
<div className='preview-integrations-plugins'>
{items.map((item) => {
return (
<div
key={item.id}
className={classnames('preview-integrations-plugins-item', {'preview-integrations-plugins-item__readonly': !item.installed && !pluginInstallationPossible})}
>
<div className='preview-integrations-plugins-item__illustration'>
<img src={item.icon}/>
</div>
<div className='preview-integrations-plugins-item__name'>
{item.name}<br/>
<span className='preview-integrations-plugins-item__name-sub'>
{makeIntegrationSubtext(item)}
</span>
</div>
{item.installed &&
<div className='preview-integrations-plugins-item__icon icon-check-circle preview-integrations-plugins-item__icon_blue'/>}
</div>);
})}
</div>
{warningMessage &&
<>
<div className='preview-integrations-warning'>
<div className='icon-alert-outline'/>
<div className='preview-integrations-warning-message'> {warningMessage} </div>
</div>
<NotifyAdminCTA
callerInfo={`${MattermostFeatures.PLUGIN_FEATURE}-${categoryId}`}
ctaText={notifyAdminCTA}
notifyRequestData={{
required_plan: pluginsToInstall.map((plugin) => plugin.id).join(','),
required_feature: `${MattermostFeatures.PLUGIN_FEATURE}-${categoryId}`,
trial_notification: false,
}}
/>
</>}
</div>
);
};
const GenericPreview = ({items, onUpdateIllustration}: GenericPreviewSectionProps) => {
const updateIllustration = (e: React.MouseEvent<HTMLAnchorElement>, illustration: string) => {
e.preventDefault();
onUpdateIllustration?.(illustration);
};
if (!items || items.length === 0) {
return null;
}
let list: ReactNode = (<li key={items[0].id}>{items[0].name}</li>);
if (items.length > 1) {
list = items.map((c) => (
<li key={c.id}>
<a
href='#'
onClick={(e) => updateIllustration(e, c.illustration || '')}
>
{c.name}
</a>
</li>
));
}
return (<ul>{list}</ul>);
};
const StyledPreviewSection = styled(PreviewSection)`
.included-title {
color: rgba(var(--center-channel-color-rgb), 0.56);
font-weight: 600;
text-transform: uppercase;
}
.preview-integrations {
#notify_admin_cta {
padding: 0 2px;
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-size: 11px;
line-height: 10px;
}
&-plugins {
display: flex;
flex-wrap: wrap;
margin-top: 8px;
gap: 8px;
&-item {
display: flex;
align-items: center;
width: 128px;
height: 48px;
flex-basis: 45%;
border: 1px solid rgba(var(--center-channel-text-rgb), 0.24);
border-radius: 4px;
&__readonly {
opacity: 65%;
}
&__illustration {
display: flex;
width: 24px;
height: 24px;
align-items: center;
margin: 12px 10px;
img {
position: relative !important;
width: 100%;
height: 100%;
}
}
&__name {
flex-grow: 2;
color: var(--center-channel-text);
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 16px;
&-sub {
color: rgba(var(--center-channel-color-rgb), 0.72);
font-weight: 400;
font-size: 10px;
}
}
&__icon {
align-self: flex-start;
&_blue {
color: var(--denim-button-bg);
}
}
}
}
&-warning {
display: flex;
margin: 8px 0px;
color: var(--error-text);
&-message {
margin-left: 3px;
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 16px;
}
}
}
.icon-check-circle::before {
margin-top: 2px;
margin-right: 2px;
}
.icon-download-outline::before {
margin-top: 8px;
margin-right: 8px;
}
`;
export default StyledPreviewSection;

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

@@ -1,25 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useSelector} from 'react-redux';
import {GlobalState} from 'types/store';
import {PluginComponent} from 'types/store/plugins';
type HookReturnType = {
pluggableId: string;
rhsPluggableIds: Map<string, string>;
pluginComponent?: PluginComponent;
};
export const useGetRHSPluggablesIds = (): HookReturnType => {
const rhsPlugins = useSelector((state: GlobalState) => state.plugins.components.RightHandSidebarComponent);
const pluggableId = useSelector((state: GlobalState) => state.views.rhs.pluggableId);
const rhsPluggableIds: Map<string, string> = new Map<string, string>();
rhsPlugins.forEach((plugin) => rhsPluggableIds.set(plugin.pluginId, plugin.id));
const pluginComponent = rhsPlugins.find((element: PluginComponent) => element.id === pluggableId);
return {pluggableId, rhsPluggableIds, pluginComponent};
};

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

@@ -1,385 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react';
import classnames from 'classnames';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import styled from 'styled-components';
import LocalizedIcon from 'components/localized_icon';
import {TTNameMapToATStatusKey, TutorialTourName} from 'components/tours/constant';
import {closeModal as closeModalAction} from 'actions/views/modals';
import {trackEvent} from 'actions/telemetry_actions';
import {showRHSPlugin} from 'actions/views/rhs';
import {fetchRemoteListing} from 'actions/marketplace';
import {loadIfNecessaryAndSwitchToChannelById} from 'actions/views/channel';
import {
clearCategories,
clearWorkTemplates,
executeWorkTemplate,
getWorkTemplateCategories,
getWorkTemplates,
onExecuteSuccess,
} from 'mattermost-redux/actions/work_templates';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {ActionResult} from 'mattermost-redux/types/actions';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {
Category,
ExecuteWorkTemplateRequest,
ExecuteWorkTemplateResponse,
Visibility,
WorkTemplate,
} from '@mattermost/types/work_templates';
import {GlobalState} from 'types/store';
import {ModalIdentifiers, suitePluginIds, TELEMETRY_CATEGORIES} from 'utils/constants';
import {AutoTourStatus} from 'components/tours';
import Customize from './components/customize';
import Menu from './components/menu';
import GenericModal from './components/modal';
import Preview from './components/preview';
import {useGetRHSPluggablesIds} from './hooks';
import {getContentCount} from './utils';
const BackIconInHeader = styled(LocalizedIcon)`
font-size: 24px;
line-height: 24px;
color: rgba(var(--center-channel-text-rbg), 0.56);
cursor: pointer;
&::before {
margin-left: 0;
margin-right: 0;
}
`;
interface ModalTitleProps {
text: string;
backArrowAction?: () => void;
}
const ModalTitle = (props: ModalTitleProps) => {
return (
<div className='work-template-modal__title'>
{props.backArrowAction &&
<BackIconInHeader
className='icon icon-arrow-left'
aria-label={'Back Icon'}
onClick={props.backArrowAction}
/>
}
<span style={{marginLeft: 18}}>{props.text}</span>
</div>
);
};
enum ModalState {
Menu = 'menu',
Customize = 'customize',
Preview = 'preview',
}
const WorkTemplateModal = () => {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const [modalState, setModalState] = useState(ModalState.Menu);
const [selectedTemplate, setSelectedTemplate] = useState<WorkTemplate | null>(null);
const [selectedName, setSelectedName] = useState<string>('');
const [selectedVisibility, setSelectedVisibility] = useState(Visibility.Public);
const [currentCategoryId, setCurrentCategoryId] = useState('');
const [isCreating, setIsCreating] = useState(false);
const [errorText, setErrorText] = useState('');
const categories = useSelector((state: GlobalState) => state.entities.worktemplates.categories);
const workTemplates = useSelector((state: GlobalState) => state.entities.worktemplates.templatesInCategory);
const config = useSelector(getConfig);
const pluginsEnabled = config.PluginsEnabled === 'true' && config.EnableMarketplace === 'true' && config.IsDefaultMarketplace === 'true';
const teamId = useSelector(getCurrentTeamId);
const playbookTemplates = useSelector((state: GlobalState) => state.entities.worktemplates.playbookTemplates);
const {rhsPluggableIds} = useGetRHSPluggablesIds();
const currentUserId = useSelector(getCurrentUserId);
useEffect(() => {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'open_modal');
}, []);
// load the categories if they are not found, or load the work templates for those categories.
useEffect(() => {
if (categories?.length) {
setCurrentCategoryId(categories[0].id);
dispatch(getWorkTemplates(categories[0].id));
return;
}
dispatch(getWorkTemplateCategories());
}, [dispatch, categories]);
useEffect(() => {
if (pluginsEnabled) {
dispatch(fetchRemoteListing());
}
}, [dispatch, pluginsEnabled]);
useEffect(() => {
return () => {
dispatch(clearCategories());
dispatch(clearWorkTemplates());
};
}, [dispatch]);
// error resetter
useEffect(() => {
setErrorText('');
}, [currentCategoryId, modalState, selectedTemplate, selectedVisibility, selectedName]);
const changeCategory = (category: Category) => {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'change_category', {category: category.id});
setCurrentCategoryId(category.id);
if (workTemplates[category.id]?.length) {
return;
}
dispatch(getWorkTemplates(category.id));
};
const closeModal = () => {
dispatch(closeModalAction(ModalIdentifiers.WORK_TEMPLATE));
};
const goToMenu = () => {
setModalState(ModalState.Menu);
setSelectedTemplate(null);
};
const handleTemplateSelected = (template: WorkTemplate, quickUse: boolean) => {
setSelectedTemplate(template);
if (quickUse) {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'quick_use', {category: template.category, template: template.id});
execute(template, '', template.visibility);
return;
}
// clear the name and set default visibility
setSelectedName('');
setSelectedVisibility(template.visibility);
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'select_template', {category: template.category, template: template.id});
setModalState(ModalState.Preview);
};
const handleOnNameChanged = (name: string) => {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'customize_name', {category: selectedTemplate?.category, template: selectedTemplate?.id});
setSelectedName(name);
};
const handleOnVisibilityChanged = (visibility: Visibility) => {
if (visibility === Visibility.Public) {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'changed_visibility_public', {category: selectedTemplate?.category, template: selectedTemplate?.id});
} else {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'customized_visibility_private', {category: selectedTemplate?.category, template: selectedTemplate?.id});
}
setSelectedVisibility(visibility);
};
/**
* Creates the necessary data in the global store as long storing in DB preferences the tourtip information
* @param template current used worktempplate
*/
const tourTipActions = async (template: WorkTemplate, firstChannelId: string) => {
const linkedProductsCount = getContentCount(template, playbookTemplates, firstChannelId);
// stepValue and pluginId are used for showing the tourtip for the used template
let pluginId;
if (linkedProductsCount.playbooks) {
pluginId = rhsPluggableIds.get(suitePluginIds.playbooks);
} else {
pluginId = rhsPluggableIds.get(suitePluginIds.boards);
}
if (!pluginId) {
return;
}
// store in the global state the plugins/integrations information related to the used template
// so we can display that data in the tourtip
await dispatch(onExecuteSuccess(linkedProductsCount));
// store the required preferences for the tourtip
const tourCategory = TutorialTourName.WORK_TEMPLATE_TUTORIAL;
const preferences = [
{
user_id: currentUserId,
category: tourCategory,
name: TTNameMapToATStatusKey[tourCategory],
value: String(AutoTourStatus.ENABLED),
},
];
await dispatch(savePreferences(currentUserId, preferences));
dispatch(showRHSPlugin(pluginId));
};
const execute = async (template: WorkTemplate, name = '', visibility: Visibility) => {
const pbTemplates = [];
for (const ctt in template.content) {
if (!Object.hasOwn(template.content, ctt)) {
continue;
}
const item = template.content[ctt];
if (item.playbook) {
const pbTemplate = playbookTemplates.find((pb) => pb.title === item.playbook.template);
if (pbTemplate) {
pbTemplates.push(pbTemplate);
}
}
}
// remove non recommended integrations
const filteredTemplate = {...template};
filteredTemplate.content = template.content.filter((item) => {
if (!item.integration) {
return true;
}
return item.integration.recommended;
});
const req: ExecuteWorkTemplateRequest = {
team_id: teamId,
name,
visibility,
work_template: filteredTemplate,
playbook_templates: pbTemplates,
};
setIsCreating(true);
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'executing', {category: template.category, template: template.id, customized_name: name !== '', customized_visibility: visibility !== template.visibility});
const {data, error} = await dispatch(executeWorkTemplate(req)) as ActionResult<ExecuteWorkTemplateResponse>;
if (error) {
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'execution_error', {category: template.category, template: template.id, customized_name: name !== '', customized_visibility: visibility !== template.visibility, error: error.message});
setErrorText(error.message);
return;
}
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'execution_success', {category: template.category, template: template.id, customized_name: name !== '', customized_visibility: visibility !== template.visibility});
let firstChannelId = '';
if (data?.channel_with_playbook_ids.length) {
firstChannelId = data.channel_with_playbook_ids[0];
} else if (data?.channel_ids.length) {
firstChannelId = data.channel_ids[0];
}
if (firstChannelId) {
dispatch(loadIfNecessaryAndSwitchToChannelById(firstChannelId));
}
await tourTipActions(template, firstChannelId);
setIsCreating(false);
closeModal();
};
const trackAction = (action: string, actionFn: () => void) => {
return () => {
let props = {};
if (selectedTemplate) {
props = {category: selectedTemplate?.category, template: selectedTemplate?.id};
}
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, action, props);
actionFn();
};
};
let title;
let cancelButtonText;
let cancelButtonAction;
let backArrowAction;
let confirmButtonText;
let confirmButtonAction;
switch (modalState) {
case ModalState.Menu:
title = formatMessage({id: 'work_templates.menu.modal_title', defaultMessage: 'Create from a template'});
break;
case ModalState.Preview:
title = formatMessage({id: 'work_templates.preview.modal_title', defaultMessage: 'Preview {useCase}'}, {useCase: selectedTemplate?.useCase});
cancelButtonText = formatMessage({id: 'work_templates.preview.modal_cancel_button', defaultMessage: 'Back'});
cancelButtonAction = trackAction('btn_back_to_menu', goToMenu);
backArrowAction = trackAction('arrow_back_to_menu', goToMenu);
confirmButtonText = formatMessage({id: 'work_templates.preview.modal_next_button', defaultMessage: 'Next'});
confirmButtonAction = trackAction('btn_go_to_customize', () => setModalState(ModalState.Customize));
break;
case ModalState.Customize:
title = formatMessage({id: 'work_templates.customize.modal_title', defaultMessage: 'Name your {useCase}'}, {useCase: selectedTemplate?.useCase});
cancelButtonText = formatMessage({id: 'work_templates.customize.modal_cancel_button', defaultMessage: 'Back'});
cancelButtonAction = trackAction('btn_back_to_preview', () => setModalState(ModalState.Preview));
backArrowAction = trackAction('arrow_back_to_preview', () => setModalState(ModalState.Preview));
confirmButtonText = formatMessage({id: 'work_templates.customize.modal_create_button', defaultMessage: 'Create'});
confirmButtonAction = trackAction('btn_execute', () => execute(selectedTemplate!, selectedName, selectedVisibility));
break;
}
return (
<GenericModal
id='work-template-modal'
className={classnames('work-template-modal', `work-template-modal--${modalState}`)}
modalHeaderText={
<ModalTitle
text={title}
backArrowAction={backArrowAction}
/>
}
compassDesign={true}
onExited={closeModal}
cancelButtonText={cancelButtonText}
handleCancel={cancelButtonAction}
confirmButtonText={confirmButtonText}
handleConfirm={confirmButtonAction}
isConfirmDisabled={isCreating || (modalState === ModalState.Customize && errorText !== '')}
autoCloseOnCancelButton={false}
autoCloseOnConfirmButton={false}
errorText={errorText}
>
{modalState === ModalState.Menu && (
<Menu
categories={categories}
onTemplateSelected={handleTemplateSelected}
changeCategory={changeCategory}
workTemplates={workTemplates}
currentCategoryId={currentCategoryId}
disableQuickUse={isCreating}
/>
)}
{(modalState === ModalState.Preview && selectedTemplate) && (
<Preview
template={selectedTemplate}
pluginsEnabled={pluginsEnabled}
/>
)}
{(modalState === ModalState.Customize && selectedTemplate) && (
<Customize
name={selectedName}
visibility={selectedVisibility}
onNameChanged={handleOnNameChanged}
onVisibilityChanged={handleOnVisibilityChanged}
template={selectedTemplate}
/>
)}
</GenericModal>
);
};
export default WorkTemplateModal;

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

@@ -1,52 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {PlaybookTemplateType, WorkTemplate} from '@mattermost/types/work_templates';
export function getTemplateDefaultIllustration(template: WorkTemplate): string {
const channels = template.content.filter((c) => c.channel).map((c) => c.channel!);
if (channels.length) {
return channels[0].illustration;
}
if (template.description.channel.illustration) {
return template.description.channel.illustration;
}
const boards = template.content.filter((c) => c.board).map((c) => c.board!);
if (boards.length) {
return boards[0].illustration;
}
if (template.description.board.illustration) {
return template.description.board.illustration;
}
const playbooks = template.content.filter((c) => c.playbook).map((c) => c.playbook!);
if (playbooks.length) {
return playbooks[0].illustration;
}
if (template.description.playbook.illustration) {
return template.description.playbook.illustration;
}
return '';
}
export const getContentCount = (template: WorkTemplate, playbookTemplates: PlaybookTemplateType[], channelId: string) => {
const res = {
playbooks: 0,
boards: 0,
channelId,
};
for (const item of template.content) {
if (item.playbook) {
const pbTemplate = playbookTemplates.find((pb) => pb.title === item.playbook.template);
if (pbTemplate) {
res.playbooks++;
}
} else if (item.board) {
res.boards++;
}
}
return res;
};

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

@@ -3476,8 +3476,8 @@
"file_search_result_item.open_in_channel": "Open in channel",
"file_upload.disabled": "File attachments are disabled.",
"file_upload.drag_folder": "Folders cannot be uploaded. Please drag all files separately.",
"file_upload.fileAbove": "File above {max}MB cannot be uploaded: {filename}",
"file_upload.filesAbove": "Files above {max}MB cannot be uploaded: {filenames}",
"file_upload.fileAbove": "File above {max}MB could not be uploaded: {filename}",
"file_upload.filesAbove": "Files above {max}MB could not be uploaded: {filenames}",
"file_upload.generic_error": "There was a problem uploading your files.",
"file_upload.limited": "Uploads limited to {count, number} files maximum. Please use additional posts for more files.",
"file_upload.menuAriaLabel": "Upload type selector",
@@ -4255,9 +4255,6 @@
"onboarding_wizard.plugins.zoom": "Zoom",
"onboarding_wizard.plugins.zoom.tooltip": "Start Zoom audio and video conferencing calls in Mattermost with a single click",
"onboarding_wizard.previous": "Previous",
"onboarding_wizard.roles.description": "Well use this to suggest templates to help get you started.",
"onboarding_wizard.roles.other_input_placeholder": "Please share your primary function",
"onboarding_wizard.roles.title": "What is your primary function?",
"onboarding_wizard.self_hosted_plugins.description": "Choose the tools you work with, and we'll add them to your workspace. Additional set up may be needed later.",
"onboarding_wizard.self_hosted_plugins.title": "What tools do you use?",
"onboarding_wizard.skip-button": "Skip",
@@ -4272,7 +4269,6 @@
"onboardingTask.checklist.no_thanks": "No, thanks",
"onboardingTask.checklist.start_enterprise_now": "Start your free Enterprise trial now!",
"onboardingTask.checklist.task_complete_your_profile": "Complete your profile.",
"onboardingTask.checklist.task_create_from_work_template": "Create from a template.",
"onboardingTask.checklist.task_download_mm_apps": "Download the Desktop and Mobile Apps.",
"onboardingTask.checklist.task_explore_other_tools_in_platform": "Explore other tools in the platform.",
"onboardingTask.checklist.task_invite_team_members": "Invite team members to the workspace.",
@@ -4367,14 +4363,6 @@
"picture_selector.select_button.ariaLabel": "Select picture",
"plan.cloud": "Cloud",
"plan.self_serve": "Self-serve",
"pluggable_rhs.tourtip.boards.access": "Access your linked boards from the Boards icon on the right hand App bar.",
"pluggable_rhs.tourtip.boards.click": "Click into boards from this right panel.",
"pluggable_rhs.tourtip.boards.review": "Review board updates from your channels.",
"pluggable_rhs.tourtip.boards.title": "Access your {count} linked {num, plural, one {board} other {boards}}!",
"pluggable_rhs.tourtip.playbooks.access": "Access your linked playbooks from the Playbooks icon on the right hand App bar.",
"pluggable_rhs.tourtip.playbooks.click": "Click into playbooks from this right panel.",
"pluggable_rhs.tourtip.playbooks.review": "Review playbook updates from your channels.",
"pluggable_rhs.tourtip.playbooks.title": "Access your {count} linked {num, plural, one {playbook} other {playbooks}}.",
"pluggable.errorOccurred": "An error occurred in the {pluginId} plugin.",
"pluggable.errorRefresh": "Refresh?",
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.",
@@ -4877,8 +4865,6 @@
"sidebar_left.add_channel_dropdown.dropdownAriaLabel": "Add Channel Dropdown",
"sidebar_left.add_channel_dropdown.invitePeople": "Invite people",
"sidebar_left.add_channel_dropdown.invitePeopleExtraText": "Add people to the team",
"sidebar_left.add_channel_dropdown.work_template": "Create from a template",
"sidebar_left.add_channel_dropdown.work_template_extra": "Link channels, boards, and playbooks together",
"sidebar_left.addChannelsCta": "Add channels",
"sidebar_left.channel_filter.filterByUnread": "Filter by unread",
"sidebar_left.channel_filter.filterUnreadAria": "unreads filter",
@@ -5102,7 +5088,6 @@
"team.button.tooltip": "Ctrl|Alt|{order}",
"team.button.tooltip.mac": "⌘|⌥|{order}",
"team.button.unread.ariaLabel": "{teamName} team unread",
"templates_command.disabled": "Templates are disabled. Please contact your System Administrator for details.",
"terms_of_service.agreeButton": "I Agree",
"terms_of_service.api_error": "Unable to complete the request. If this issue persists, contact your System Administrator.",
"terms_of_service.disagreeButton": "I Disagree",
@@ -5703,40 +5688,6 @@
"widgets.users_emails_input.loading": "Loading",
"widgets.users_emails_input.no_user_found_matching": "No one found matching **{text}**. Enter their email to invite them.",
"widgets.users_emails_input.valid_email": "Add **{email}**",
"work_templates.customize.modal_cancel_button": "Back",
"work_templates.customize.modal_create_button": "Create",
"work_templates.customize.modal_title": "Name your {useCase}",
"work_templates.customize.name_description": "This will help you and others find your project items. You can always edit this later.",
"work_templates.customize.name_input_placeholder": "e.g. Web app, Growth, etc.",
"work_templates.customize.name_label_all": "Name your channel, board, and playbook",
"work_templates.customize.name_label_channels_boards": "Name your channel and board",
"work_templates.customize.name_label_channels_playbooks": "Name your channel and playbook",
"work_templates.customize.private_channel_permission_issue": "You do not have permission to create private channels.",
"work_templates.customize.private_playbook_license_issue": "Private playbooks requires an Enterprise license.",
"work_templates.customize.private_playbook_permission_issue": "You do not have permission to create private playbooks.",
"work_templates.customize.public_channel_permission_issue": "You do not have permission to create public channels.",
"work_templates.customize.public_playbook_permission_issue": "You do not have permission to create public playbooks.",
"work_templates.customize.visibility_title": "Who should have access to this?",
"work_templates.menu.modal_title": "Create from a template",
"work_templates.menu.quick_use": "Quick use",
"work_templates.menu.template_title": "TEMPLATE",
"work_templates.menu.usecase_boards_count": "{boardsCount, plural, =1 {# board} other {# boards}}",
"work_templates.menu.usecase_channels_count": "{channelsCount, plural, =1 {# channel} other {# channels}}",
"work_templates.menu.usecase_playbooks_count": "{playbooksCount, plural, =1 {# playbook} other {# playbooks}}",
"work_templates.preview.accordion_title_boards": "Boards",
"work_templates.preview.accordion_title_channels": "Channels",
"work_templates.preview.accordion_title_playbooks": "Playbooks",
"work_templates.preview.integrations.admin_install.multiple_plugin": "Integrations will not be added until admin installs them.",
"work_templates.preview.integrations.admin_install.notify": "Notify admin to install integrations",
"work_templates.preview.integrations.admin_install.single_plugin": "{plugin} will not be added until admin installs it.",
"work_templates.preview.integrations.already_installed": "Already installed",
"work_templates.preview.integrations.app_install": "App Install",
"work_templates.preview.integrations.to_be_installed": "To be installed",
"work_templates.preview.modal_cancel_button": "Back",
"work_templates.preview.modal_next_button": "Next",
"work_templates.preview.modal_title": "Preview {useCase}",
"work_templates.preview.section.included": "Included",
"work_templates.preview.what_you_get": "Here's what you'll get:",
"workspace_limits.archived_file.archived": "This file is archived",
"workspace_limits.archived_file.archived_compact": "(archived)",
"workspace_limits.archived_file.tooltip_description": "Your workspace has hit the file storage limit of {storageLimit}. To view this again, upgrade to a paid plan",

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 143 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 53 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 259 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 419 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 296 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 306 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 236 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 568 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 119 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 568 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 182 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 80 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 22 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 568 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 119 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 567 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 98 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 567 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 113 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 567 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 68 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 109 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 565 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 568 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 119 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 567 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 88 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 567 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 113 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 567 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 68 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 109 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 565 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 568 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 119 KiB

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше