[MM-52926] Deprecating work templates (#23466)
Co-authored-by: Mattermost Build <build@mattermost.com>
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 you’re 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 you’re 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, that’s 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 you’re 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, that’s 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 you’re 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 you’re 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 you’re 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 you’re 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 you’re 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 you’re 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, that’s 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 you’re 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 you’re 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 you’re 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, that’s 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 you’re 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={'We’ll 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": "We’ll 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 |
Двоичные данные
webapp/channels/src/images/worktemplates/boards/roadmap.png
|
До Ширина: | Высота: | Размер: 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 |