Merge branch 'master' of github.com:mattermost/mattermost-server into MM-50966-in-product-expansion-backend

Этот коммит содержится в:
Conor Macpherson
2023-03-27 13:29:20 -04:00
родитель 336176f8cb 1cf0cff9c6
Коммит 1b9d97209d
107 изменённых файлов: 2137 добавлений и 929 удалений

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

@@ -7,7 +7,7 @@ stages:
include: include:
- project: mattermost/ci/mattermost-server - project: mattermost/ci/mattermost-server
ref: monorepo-testing ref: master
file: private.yml file: private.yml
variables: variables:

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

@@ -11,8 +11,8 @@ You may be licensed to use source code to create compiled versions not produced
1. Under the Free Software Foundations GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or 1. Under the Free Software Foundations GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or
2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com 2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com
You are licensed to use the source code in Admin Tools and Configuration Files (templates/, config/default.json, i18n/, model/, You are licensed to use the source code in Admin Tools and Configuration Files (server/templates/, server/i18n/, model/,
plugin/ and all subdirectories thereof) under the Apache License v2.0. plugin/, server/boards/, server/playbooks/, webapp/ and all subdirectories thereof) under the Apache License v2.0.
We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not
link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and

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

@@ -21,10 +21,11 @@ type BootstrapSelfHostedSignupResponseInternal struct {
// email contained in token, so not in the request body. // email contained in token, so not in the request body.
type SelfHostedCustomerForm struct { type SelfHostedCustomerForm struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
BillingAddress *Address `json:"billing_address"` BillingAddress *Address `json:"billing_address"`
Organization string `json:"organization"` ShippingAddress *Address `json:"shipping_address"`
Organization string `json:"organization"`
} }
type SelfHostedConfirmPaymentMethodRequest struct { type SelfHostedConfirmPaymentMethodRequest struct {

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

@@ -140,6 +140,7 @@ func TestAddMemberToBoard(t *testing.T) {
} }
func TestPatchBoard(t *testing.T) { func TestPatchBoard(t *testing.T) {
t.Skip("MM-51699")
th, tearDown := SetupTestHelper(t) th, tearDown := SetupTestHelper(t)
defer tearDown() defer tearDown()

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

@@ -231,6 +231,9 @@ func (bm *BoardsMigrator) MigrateToStep(step int) error {
func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor { func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor {
return map[int]foundation.Interceptor{ return map[int]foundation.Interceptor{
18: bm.store.RunDeletedMembershipBoardsMigration, 18: bm.store.RunDeletedMembershipBoardsMigration,
35: func() error {
return bm.store.RunDeDuplicateCategoryBoardsMigration(35)
},
} }
} }

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

@@ -863,10 +863,8 @@ func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) {
} }
func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error { func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error {
query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " + query := "DELETE FROM " + s.tablePrefix + "category_boards WHERE id NOT IN " +
"FROM " + s.tablePrefix + "category_boards) " + "(SELECT * FROM ( SELECT min(id) FROM " + s.tablePrefix + "category_boards GROUP BY user_id, board_id ) as data)"
"DELETE " + s.tablePrefix + "category_boards FROM " + s.tablePrefix + "category_boards " +
"JOIN duplicates USING(id) WHERE duplicates.rownum > 1;"
if _, err := s.db.Exec(query); err != nil { if _, err := s.db.Exec(query); err != nil {
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err)) s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
} }

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

@@ -7,6 +7,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store/sqlstore/migrationstests"
"github.com/mgdelacroix/foundation"
"github.com/mattermost/mattermost-server/v6/server/boards/model" "github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -263,3 +266,23 @@ func TestCheckForMismatchedCollation(t *testing.T) {
} }
}) })
} }
func TestRunDeDuplicateCategoryBoardsMigration(t *testing.T) {
RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) {
th, tearDown := migrationstests.SetupTestHelper(t, f)
defer tearDown()
th.F().MigrateToStepSkippingLastInterceptor(35).
ExecFile("./fixtures/testDeDuplicateCategoryBoardsMigration.sql")
th.F().RunInterceptor(35)
// verifying count of rows
var count int
countQuery := "SELECT COUNT(*) FROM focalboard_category_boards"
row := th.F().DB().QueryRow(countQuery)
err := row.Scan(&count)
assert.NoError(t, err)
assert.Equal(t, 4, count)
})
}

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

@@ -0,0 +1,9 @@
INSERT INTO focalboard_category_boards(id, user_id, category_id, board_id, create_at, update_at, sort_order)
VALUES
('id_1', 'user_id_1', 'category_id_1', 'board_id_1', 0, 0, 0),
('id_2', 'user_id_1', 'category_id_2', 'board_id_1', 0, 0, 0),
('id_3', 'user_id_1', 'category_id_3', 'board_id_1', 0, 0, 0),
('id_4', 'user_id_2', 'category_id_4', 'board_id_2', 0, 0, 0),
('id_5', 'user_id_2', 'category_id_5', 'board_id_2', 0, 0, 0),
('id_6', 'user_id_3', 'category_id_6', 'board_id_3', 0, 0, 0),
('id_7', 'user_id_4', 'category_id_6', 'board_id_4', 0, 0, 0);

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

@@ -22,6 +22,10 @@ func (th *TestHelper) IsMySQL() bool {
return th.f.DB().DriverName() == "mysql" return th.f.DB().DriverName() == "mysql"
} }
func (th *TestHelper) F() *foundation.Foundation {
return th.f
}
func SetupTestHelper(t *testing.T, f *foundation.Foundation) (*TestHelper, func()) { func SetupTestHelper(t *testing.T, f *foundation.Foundation) (*TestHelper, func()) {
th := &TestHelper{t, f} th := &TestHelper{t, f}

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

@@ -50,6 +50,7 @@ func NewStoreType(name string, driver string, skipMigrations bool) *storeType {
DB: sqlDB, DB: sqlDB,
IsPlugin: false, // ToDo: to be removed IsPlugin: false, // ToDo: to be removed
} }
store, err := New(storeParams) store, err := New(storeParams)
if err != nil { if err != nil {
panic(fmt.Sprintf("cannot create store: %s", err)) panic(fmt.Sprintf("cannot create store: %s", err))

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

@@ -17,7 +17,7 @@ import (
) )
const ( const (
rudderKey = "placeholder_rudder_key" rudderKey = "placeholder_boards_rudder_key"
rudderDataplaneURL = "placeholder_rudder_dataplane_url" rudderDataplaneURL = "placeholder_rudder_dataplane_url"
timeBetweenTelemetryChecks = 10 * time.Minute timeBetweenTelemetryChecks = 10 * time.Minute
) )

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

@@ -71,10 +71,8 @@ type TestHelper struct {
IncludeCacheLayer bool IncludeCacheLayer bool
LogBuffer *mlog.Buffer LogBuffer *mlog.Buffer
TestLogger *mlog.Logger TestLogger *mlog.Logger
boardsProductEnvValue string
playbooksDisableEnvValue string
} }
var mainHelper *testlib.MainHelper var mainHelper *testlib.MainHelper
@@ -104,17 +102,6 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = false *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false *memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
// disable Boards through the feature flag
boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct")
os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct")
memoryConfig.FeatureFlags.BoardsProduct = false
// disable Playbooks (temporarily) as it causes many more mocked methods to get
// called, and cannot receieve a mocked database.
playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS")
os.Setenv("MM_DISABLE_PLAYBOOKS", "true")
if updateConfig != nil { if updateConfig != nil {
updateConfig(memoryConfig) updateConfig(memoryConfig)
} }
@@ -153,15 +140,13 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent
} }
th := &TestHelper{ th := &TestHelper{
App: app.New(app.ServerConnector(s.Channels())), App: app.New(app.ServerConnector(s.Channels())),
Server: s, Server: s,
ConfigStore: configStore, ConfigStore: configStore,
IncludeCacheLayer: includeCache, IncludeCacheLayer: includeCache,
Context: request.EmptyContext(testLogger), Context: request.EmptyContext(testLogger),
TestLogger: testLogger, TestLogger: testLogger,
LogBuffer: buffer, LogBuffer: buffer,
boardsProductEnvValue: boardsProductEnvValue,
playbooksDisableEnvValue: playbooksDisableEnvValue,
} }
th.Context.SetLogger(testLogger) th.Context.SetLogger(testLogger)
@@ -386,17 +371,6 @@ func (th *TestHelper) ShutdownApp() {
} }
func (th *TestHelper) TearDown() { func (th *TestHelper) TearDown() {
// reset board and playbooks product setting to original
if th.boardsProductEnvValue != "" {
os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue)
}
if th.playbooksDisableEnvValue != "" {
os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue)
} else {
os.Unsetenv("MM_DISABLE_PLAYBOOKS")
}
if th.IncludeCacheLayer { if th.IncludeCacheLayer {
// Clean all the caches // Clean all the caches
th.App.Srv().InvalidateAllCaches() th.App.Srv().InvalidateAllCaches()

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

@@ -13,6 +13,8 @@ import (
"reflect" "reflect"
"time" "time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils" "github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog" "github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
@@ -274,6 +276,10 @@ func selfHostedInvoices(c *Context, w http.ResponseWriter, r *http.Request) {
invoices, err := c.App.Cloud().GetSelfHostedInvoices() invoices, err := c.App.Cloud().GetSelfHostedInvoices()
if err != nil { if err != nil {
if err.Error() == "404" {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotFound).Wrap(errors.New("invoices for license not found"))
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return return
} }

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

@@ -11,8 +11,10 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
) )
/* Temporarily comment out until MM-11108 /* Temporarily comment out until MM-11108
@@ -37,9 +39,26 @@ func init() {
} }
func TestUnitUpdateConfig(t *testing.T) { func TestUnitUpdateConfig(t *testing.T) {
th := Setup(t) th := SetupWithStoreMock(t)
defer th.TearDown() defer th.TearDown()
mockStore := th.App.Srv().Store().(*mocks.Store)
mockUserStore := mocks.UserStore{}
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
mockPostStore := mocks.PostStore{}
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
mockSystemStore := mocks.SystemStore{}
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
mockLicenseStore := mocks.LicenseStore{}
mockLicenseStore.On("Get", "").Return(&model.LicenseRecord{}, nil)
mockStore.On("User").Return(&mockUserStore)
mockStore.On("Post").Return(&mockPostStore)
mockStore.On("System").Return(&mockSystemStore)
mockStore.On("License").Return(&mockLicenseStore)
mockStore.On("GetDBSchemaVersion").Return(1, nil)
prev := *th.App.Config().ServiceSettings.SiteURL prev := *th.App.Config().ServiceSettings.SiteURL
var called int32 var called int32

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

@@ -42,9 +42,7 @@ type TestHelper struct {
TestLogger *mlog.Logger TestLogger *mlog.Logger
IncludeCacheLayer bool IncludeCacheLayer bool
tempWorkspace string tempWorkspace string
boardsProductEnvValue string
playbooksDisableEnvValue string
} }
func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, options []Option, tb testing.TB) *TestHelper { func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, options []Option, tb testing.TB) *TestHelper {
@@ -62,17 +60,6 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
*memoryConfig.LogSettings.EnableSentry = false // disable error reporting during tests *memoryConfig.LogSettings.EnableSentry = false // disable error reporting during tests
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = false *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false
// disable Boards through the feature flag
boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct")
os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct")
memoryConfig.FeatureFlags.BoardsProduct = false
// disable Playbooks (temporarily) as it causes many more mocked methods to get
// called, and cannot receieve a mocked database.
playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS")
os.Setenv("MM_DISABLE_PLAYBOOKS", "true")
configStore.Set(memoryConfig) configStore.Set(memoryConfig)
buffer := &mlog.Buffer{} buffer := &mlog.Buffer{}
@@ -103,14 +90,12 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
} }
th := &TestHelper{ th := &TestHelper{
App: New(ServerConnector(s.Channels())), App: New(ServerConnector(s.Channels())),
Context: request.EmptyContext(testLogger), Context: request.EmptyContext(testLogger),
Server: s, Server: s,
LogBuffer: buffer, LogBuffer: buffer,
TestLogger: testLogger, TestLogger: testLogger,
IncludeCacheLayer: includeCacheLayer, IncludeCacheLayer: includeCacheLayer,
boardsProductEnvValue: boardsProductEnvValue,
playbooksDisableEnvValue: playbooksDisableEnvValue,
} }
th.Context.SetLogger(testLogger) th.Context.SetLogger(testLogger)
@@ -184,10 +169,16 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper {
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil) statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil) statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil) statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
pluginMock := mocks.PluginStore{}
pluginMock.On("Get", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&model.PluginKeyValue{}, nil)
emptyMockStore := mocks.Store{} emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil) emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock) emptyMockStore.On("Status").Return(&statusMock)
emptyMockStore.On("Plugin").Return(&pluginMock).Maybe()
th.App.Srv().SetStore(&emptyMockStore) th.App.Srv().SetStore(&emptyMockStore)
return th return th
} }
@@ -553,17 +544,6 @@ func (th *TestHelper) ShutdownApp() {
} }
func (th *TestHelper) TearDown() { func (th *TestHelper) TearDown() {
// reset board and playbooks product setting to original
if th.boardsProductEnvValue != "" {
os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue)
}
if th.playbooksDisableEnvValue != "" {
os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue)
} else {
os.Unsetenv("MM_DISABLE_PLAYBOOKS")
}
if th.IncludeCacheLayer { if th.IncludeCacheLayer {
// Clean all the caches // Clean all the caches
th.App.Srv().InvalidateAllCaches() th.App.Srv().InvalidateAllCaches()

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

@@ -1445,13 +1445,9 @@ func TestPushNotificationRace(t *testing.T) {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
var err error var err error
s.platform, err = platform.New( s.platform, err = platform.New(platform.ServiceConfig{
platform.ServiceConfig{ ConfigStore: memoryStore,
ConfigStore: memoryStore, }, platform.SetFileStore(&fmocks.FileBackend{}))
},
platform.SetFileStore(&fmocks.FileBackend{}),
platform.StoreOverride(th.GetSqlStore()),
)
s.SetStore(mockStore) s.SetStore(mockStore)
require.NoError(t, err) require.NoError(t, err)
serviceMap := map[product.ServiceKey]any{ serviceMap := map[product.ServiceKey]any{

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

@@ -48,12 +48,12 @@ func (a *App) SaveAdminNotification(userId string, notifyData *model.NotifyAdmin
func (a *App) DoCheckForAdminNotifications(trial bool) *model.AppError { func (a *App) DoCheckForAdminNotifications(trial bool) *model.AppError {
ctx := request.EmptyContext(a.Srv().Log()) ctx := request.EmptyContext(a.Srv().Log())
currentSKU := "starter"
license := a.Srv().License() license := a.Srv().License()
if license == nil { if license != nil {
return model.NewAppError("DoCheckForAdminNotifications", "app.notify_admin.send_notification_post.app_error", nil, "No license found", http.StatusInternalServerError) currentSKU = license.SkuShortName
} }
currentSKU := license.SkuShortName
workspaceName := "" workspaceName := ""
return a.SendNotifyAdminPosts(ctx, workspaceName, currentSKU, trial) return a.SendNotifyAdminPosts(ctx, workspaceName, currentSKU, trial)

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

@@ -73,17 +73,15 @@ func (s *Server) initializeProducts(
func (s *Server) shouldStart(product string) bool { func (s *Server) shouldStart(product string) bool {
if product == "boards" { if product == "boards" {
if !s.Config().FeatureFlags.BoardsProduct { if !s.Config().FeatureFlags.BoardsProduct {
s.Log().Info("Skipping Boards init; disabled via feature flag") s.Log().Warn("Skipping boards start: not enabled via feature flag")
return false return false
} }
s.Log().Info("Allowing Boards init; enabled via feature flag")
} }
if product == "playbooks" { if product == "playbooks" {
if os.Getenv("MM_DISABLE_PLAYBOOKS") == "true" { if os.Getenv("MM_DISABLE_PLAYBOOKS") == "true" {
s.Log().Info("Skipping Playbooks init; disabled via env var") s.Log().Warn("Skipping playbooks start: disabled via env var")
return false return false
} }
s.Log().Info("Allowing Playbooks init; enabled via env var")
} }
return true return true

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

@@ -36,9 +36,7 @@ type TestHelper struct {
TestLogger *mlog.Logger TestLogger *mlog.Logger
IncludeCacheLayer bool IncludeCacheLayer bool
tempWorkspace string tempWorkspace string
boardsProductEnvValue string
playbooksDisableEnvValue string
} }
func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB, configSet func(*model.Config)) *TestHelper { func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB, configSet func(*model.Config)) *TestHelper {
@@ -53,17 +51,6 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
if configSet != nil { if configSet != nil {
configSet(memoryConfig) configSet(memoryConfig)
} }
// disable Boards through the feature flag
boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct")
os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct")
memoryConfig.FeatureFlags.BoardsProduct = false
// disable Playbooks (temporarily) as it causes many more mocked methods to get
// called, and cannot receieve a mocked database.
playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS")
os.Setenv("MM_DISABLE_PLAYBOOKS", "true")
*memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") *memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
*memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") *memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false *memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
@@ -95,14 +82,12 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
} }
th := &TestHelper{ th := &TestHelper{
App: app.New(app.ServerConnector(s.Channels())), App: app.New(app.ServerConnector(s.Channels())),
Context: request.EmptyContext(testLogger), Context: request.EmptyContext(testLogger),
Server: s, Server: s,
LogBuffer: buffer, LogBuffer: buffer,
TestLogger: testLogger, TestLogger: testLogger,
IncludeCacheLayer: includeCacheLayer, IncludeCacheLayer: includeCacheLayer,
boardsProductEnvValue: boardsProductEnvValue,
playbooksDisableEnvValue: playbooksDisableEnvValue,
} }
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 })
@@ -389,17 +374,6 @@ func (th *TestHelper) shutdownApp() {
} }
func (th *TestHelper) tearDown() { func (th *TestHelper) tearDown() {
// reset board and playbooks product setting to original
if th.boardsProductEnvValue != "" {
os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue)
}
if th.playbooksDisableEnvValue != "" {
os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue)
} else {
os.Unsetenv("MM_DISABLE_PLAYBOOKS")
}
if th.IncludeCacheLayer { if th.IncludeCacheLayer {
// Clean all the caches // Clean all the caches
th.App.Srv().InvalidateAllCaches() th.App.Srv().InvalidateAllCaches()

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

@@ -12,7 +12,7 @@ import (
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog" "github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
) )
const installPluginSchedFreq = 1 * time.Minute const installPluginSchedFreq = 24 * time.Hour
func MakeInstallPluginScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler { func MakeInstallPluginScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler {
isEnabled := func(cfg *model.Config) bool { isEnabled := func(cfg *model.Config) bool {

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

@@ -48,9 +48,6 @@ type TestHelper struct {
IncludeCacheLayer bool IncludeCacheLayer bool
TestLogger *mlog.Logger TestLogger *mlog.Logger
boardsProductEnvValue string
playbooksDisableEnvValue string
} }
func SetupWithStoreMock(tb testing.TB) *TestHelper { func SetupWithStoreMock(tb testing.TB) *TestHelper {
@@ -80,17 +77,6 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper {
*newConfig.AnnouncementSettings.AdminNoticesEnabled = false *newConfig.AnnouncementSettings.AdminNoticesEnabled = false
*newConfig.AnnouncementSettings.UserNoticesEnabled = false *newConfig.AnnouncementSettings.UserNoticesEnabled = false
*newConfig.PluginSettings.AutomaticPrepackagedPlugins = false *newConfig.PluginSettings.AutomaticPrepackagedPlugins = false
// disable Boards through the feature flag
boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct")
os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct")
newConfig.FeatureFlags.BoardsProduct = false
// disable Playbooks (temporarily) as it causes many more mocked methods to get
// called, and cannot receieve a mocked database.
playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS")
os.Setenv("MM_DISABLE_PLAYBOOKS", "true")
memoryStore.Set(newConfig) memoryStore.Set(newConfig)
var options []app.Option var options []app.Option
options = append(options, app.ConfigStore(memoryStore)) options = append(options, app.ConfigStore(memoryStore))
@@ -148,14 +134,12 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper {
}) })
th := &TestHelper{ th := &TestHelper{
App: a, App: a,
Context: request.EmptyContext(testLogger), Context: request.EmptyContext(testLogger),
Server: s, Server: s,
Web: web, Web: web,
IncludeCacheLayer: includeCacheLayer, IncludeCacheLayer: includeCacheLayer,
TestLogger: testLogger, TestLogger: testLogger,
boardsProductEnvValue: boardsProductEnvValue,
playbooksDisableEnvValue: playbooksDisableEnvValue,
} }
th.Context.SetLogger(testLogger) th.Context.SetLogger(testLogger)
@@ -194,17 +178,6 @@ func (th *TestHelper) InitBasic() *TestHelper {
} }
func (th *TestHelper) TearDown() { func (th *TestHelper) TearDown() {
// reset board and playbooks product setting to original
if th.boardsProductEnvValue != "" {
os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue)
}
if th.playbooksDisableEnvValue != "" {
os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue)
} else {
os.Unsetenv("MM_DISABLE_PLAYBOOKS")
}
if th.IncludeCacheLayer { if th.IncludeCacheLayer {
// Clean all the caches // Clean all the caches
th.App.Srv().InvalidateAllCaches() th.App.Srv().InvalidateAllCaches()

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

@@ -10013,5 +10013,213 @@
{ {
"id": "app.oauth.remove_auth_data_by_client_id.app_error", "id": "app.oauth.remove_auth_data_by_client_id.app_error",
"translation": "Oauth-Daten können nicht entfernt werden." "translation": "Oauth-Daten können nicht entfernt werden."
},
{
"id": "app.user.run.update_status.title",
"translation": "Aktueller Stand"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Status aktualisieren"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Erinnerung an das nächste Update"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Bringe die Beteiligten auf den neuesten Stand. Dieser Beitrag wird in einem Kanal veröffentlicht.",
"other": "Bringe die Beteiligten auf den neuesten Stand. Dieser Beitrag wird in {{.Count}} Kanälen veröffentlicht."
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Markiere den Durchlauf auch als beendet"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Durchlauf beenden"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Änderung seit der letzten Aktualisierung"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} hat die Statusaktualisierungen für [{{.RunName}}]({{.RunURL}}) aktiviert"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} hat die Statusaktualisierungen für [{{.RunName}}]({{.RunURL}}) deaktiviert"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} hat eine Statusaktualisierung für [{{.RunName}}]({{.RunURL}}) angefordert. \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} ist ein Teilnehmer und möchte diesem Kanal beitreten. Jedes Mitglied des Kanals kann ihn einladen.\n"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Beenden des Durchlaufs bestätigen"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Durchlauf beenden"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Es gibt **eine offene Aufgabe**. Bist du sicher, dass du den Durchlauf *{{.RunName}}* für alle Teilnehmer beenden willst?",
"other": "Es gibt **{{.Count}} offene Aufgaben**. Bist du sicher, dass du den Durchlauf *{{.RunName}}* für alle Teilnehmer beenden willst?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Zur Zeitleiste hinzufügen"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Kurze Zusammenfassung auf der Zeitachse"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max. 64 Zeichen"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Zusammenfassung"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Zur Zeitleiste hinzufügen"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook-Durchlauf"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Neue Aufgabe hinzufügen"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Aufgabe hinzufügen"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Name"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Beschreibung"
},
{
"id": "app.user.new_run.title",
"translation": "Playbook starten"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Starte Durchlauf"
},
{
"id": "app.user.new_run.run_name",
"translation": "Name des Durchlaufs"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.intro",
"translation": "**Eigentümer** {{.Username}}"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Du hast keine zugewiesene Aufgabe."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Du hast eine zugewiesene Aufgabe, die jetzt fällig ist:",
"other": "Du hast {{.Count}} zugewiesene Aufgaben, die jetzt fällig sind:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Du hast eine zugewiesene Aufgabe:",
"other": "Du hast {{.Count}} zugewiesene Aufgaben:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Deine zugewiesenen Aufgaben"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Gestern fällig"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Fällig vor {{.Count}} Tagen"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Heute fällig"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Fällig in einem Tag",
"other": "Fällig in {{.Count}} Tagen"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Du hast **eine zugewiesene Aufgabe, die nach dem heutige Tag fällig ist**.",
"other": "Du hast **{{.Count}} zugewiesene Aufgaben, die nach dem heutige Tag fällig sind**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Bitte benutze `/playbook todo` um alle deine Aufgaben anzuzeigen."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Du hast keinen aktiven Durchlauf."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Du hast einen aktiven Durchlauf:",
"other": "Du hast {{.Count}} aktive Durchläufe:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Aktive Durchläufe"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Du hast keine überfälligen Durchläufe."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Du hast einen überfälligen Durchlauf für ein Statusupdate:",
"other": "Du hast {{.Count}} überfällige Durchläufe für ein Statusupdate:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Überfällige Statusaktualisierungen"
},
{
"id": "app.command.execute.error",
"translation": "Kann Befehl nicht ausführen."
} }
] ]

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

@@ -9546,5 +9546,9 @@
{ {
"id": "api.admin.syncables_error", "id": "api.admin.syncables_error",
"translation": "Error al agregar usuario a grupo-equipos y grupo-canales" "translation": "Error al agregar usuario a grupo-equipos y grupo-canales"
},
{
"id": "api.command_templates.name",
"translation": "plantillas"
} }
] ]

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

@@ -9998,5 +9998,213 @@
{ {
"id": "api.command_templates.unsupported.app_error", "id": "api.command_templates.unsupported.app_error",
"translation": "あなたのデバイスではテンプレートコマンドはサポートされていません。" "translation": "あなたのデバイスではテンプレートコマンドはサポートされていません。"
},
{
"id": "app.user.run.update_status.title",
"translation": "ステータスの更新"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "ステータスを更新"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "次回更新のリマインド"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"other": "関係者に更新内容を提供します。この投稿は {{.Count}} チャンネルに配信されます。"
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "また、実行を終了としてマークする"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "実行を終了する"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "前回更新時からの変更点"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} は [{{.RunName}}]({{.RunURL}}) のステータス更新を有効化しました"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} は [{{.RunName}}]({{.RunURL}}) のステータス更新を無効化しました"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} は [{{.RunName}}]({{.RunURL}}) のステータスの更新を要求しました。 \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} は実行の参加者で、このチャンネルへの参加を希望しています。チャンネルのメンバーなら誰でも招待できます。\n"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "実行終了の確認"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "実行を終了する"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"other": "**{{.Count}} 個の未解決タスク**があります。 本当に実行 *{{.RunName}}* を終了してもよろしいですか?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "実行のタイムラインに追加する"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "タイムラインに表示される短い要約"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "最大 64 文字"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "概要"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "実行のタイムラインに追加する"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbookを実行"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "新しいタスクを追加"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "タスクを追加"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "名前"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "説明"
},
{
"id": "app.user.new_run.title",
"translation": "Playbookを実行する"
},
{
"id": "app.user.new_run.submit_label",
"translation": "実行開始"
},
{
"id": "app.user.new_run.run_name",
"translation": "実行名"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.intro",
"translation": "**オーナー** {{.Username}}"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "割り当てられたタスクがありません。"
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"other": "対応期限を迎えている{{.Count}}タスクが割り当てられています:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"other": "{{.Count}}タスクが割り当てられています:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "あなたに割り当てられたタスク"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "昨日で期限切れ"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "{{.Count}}日前に期限切れ"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "今日が期限です"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"other": "期限は{{.Count}}日後です"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"other": "**今日で期限切れとなるタスクが {{.Count}} 件**割り当てられています。"
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "`/playbook todo`を使用すると、あなたのすべてのタスクを確認することができます。"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "進行中の実行はありません。"
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"other": "現在、 {{.Count}} の実行が進行中です:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "進行中の実行"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "期限切れの実行は 0 です。"
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"other": "ステータス更新の期日が過ぎた実行が {{.Count}} あります:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "期限切れステータスの更新"
},
{
"id": "app.oauth.remove_auth_data_by_client_id.app_error",
"translation": "oauth データを削除することができませんでした。"
},
{
"id": "app.command.execute.error",
"translation": "コマンドを実行できませんでした。"
},
{
"id": "api.templates.license_up_for_renewal_contact_sales",
"translation": "営業に問い合わせる"
} }
] ]

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

@@ -10014,5 +10014,29 @@
{ {
"id": "app.oauth.remove_auth_data_by_client_id.app_error", "id": "app.oauth.remove_auth_data_by_client_id.app_error",
"translation": "Nie można usunąć danych oauth." "translation": "Nie można usunąć danych oauth."
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Uruchomienia w Trakcie"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Masz 0 zaległych uruchomień."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"few": "Masz {{.Count}} zaległości w aktualizacji statusu:",
"many": "Masz {{.Count}} zaległości w aktualizacji statusu:",
"one": "Masz {{.Count}} zaległość w aktualizacji statusu:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Zaległe aktualizacje statusu"
},
{
"id": "app.command.execute.error",
"translation": "Nie można wykonać polecenia."
} }
] ]

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

@@ -9858,5 +9858,353 @@
{ {
"id": "api.command_templates.unsupported.app_error", "id": "api.command_templates.unsupported.app_error",
"translation": "您的设备不支持模板命令。" "translation": "您的设备不支持模板命令。"
},
{
"id": "worktemplate.product_teams.sprint_planning.integration",
"translation": "项目面板可以使迭代计划前所未来的容易。频道可以用来对话和保证问题的被关注。迭代计划面板可以使所以有本周对任务的关注,回顾面板让团队作为一个整体不断改进。"
},
{
"id": "worktemplate.product_teams.sprint_planning.channel",
"translation": "项目面板可以使迭代计划前所未来的容易。频道可以用来对话和保证问题的被关注。迭代计划面板可以使所以有本周对任务的关注,回顾面板让团队作为一个整体不断改进。"
},
{
"id": "worktemplate.product_teams.sprint_planning.board",
"translation": "项目面板可以使迭代计划前所未来的容易。频道可以用来对话和保证问题的被关注。迭代计划面板可以使所以有本周对任务的关注,回顾面板让团队作为一个整体不断改进。"
},
{
"id": "worktemplate.product_teams.product_roadmap.channel",
"translation": "这里描述了为什么需要面板"
},
{
"id": "worktemplate.product_teams.product_roadmap.board",
"translation": "这里描述了为什么需要面板"
},
{
"id": "worktemplate.product_teams.goals_and_okrs.integration",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.product_teams.goals_and_okrs.channel",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.product_teams.goals_and_okrs.board",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.product_teams.feature_release.description.playbook",
"translation": "通过建立透明的跨越整个研发团队的工作流程确保你的功能开发过程完美流畅。"
},
{
"id": "worktemplate.product_teams.feature_release.description.integration",
"translation": "在你的频道通过集成Jira和Gtihub机器人提高效率。这些会自动下载安装。"
},
{
"id": "worktemplate.product_teams.feature_release.description.channel",
"translation": "Boards,Playbooks和应用Bot可以很容易地接入功能发布频道并且和你的团队进行相关互动和讨论。"
},
{
"id": "worktemplate.product_teams.feature_release.description.board",
"translation": "使用我们的会议日程模板安排像站立会议这样的定期会议,使用我们的项目任务面板在一路上管理任务的进度。"
},
{
"id": "worktemplate.product_teams.bug_bash.playbook",
"translation": "把事情安排好并且干掉此项目里的所有bug用包含的Playbook, Board, and Channel推动项目并评估进度。"
},
{
"id": "worktemplate.product_teams.bug_bash.integration",
"translation": "把事情安排好并且干掉此项目里的所有bug用包含的Playbook, Board, and Channel推动项目并评估进度。"
},
{
"id": "worktemplate.product_teams.bug_bash.channel",
"translation": "把事情安排好并且干掉此项目里的所有bug用包含的Playbook, Board, and Channel推动项目并评估进度。"
},
{
"id": "worktemplate.product_teams.bug_bash.board",
"translation": "把事情安排好并且干掉此项目里的所有bug用包含的Playbook, Board, and Channel推动项目并评估进度。"
},
{
"id": "worktemplate.leadership.goals_and_okrs.integration",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.leadership.goals_and_okrs.channel",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.leadership.goals_and_okrs.board",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.devops.product_release.playbook",
"translation": "不要丢失此项目的任何一个步骤。从Playbook的检验清单分离成任务部署并达到项目面板的里程碑。用频道来保持所有人对事情的理解一致。"
},
{
"id": "worktemplate.devops.product_release.channel",
"translation": "不要丢失此项目的任何一个步骤。从Playbook的检验清单分离成任务部署并达到项目面板的里程碑。用频道来保持所有人对事情的理解一致。"
},
{
"id": "worktemplate.devops.product_release.board",
"translation": "不要丢失此项目的任何一个步骤。从Playbook的检验清单分离成任务部署并达到项目面板的里程碑。用频道来保持所有人对事情的理解一致。"
},
{
"id": "worktemplate.devops.incident_resolution.description.playbook",
"translation": "当到处都是问题的时候有一个能够确保一切都尽快回归正确的可重复流程是关键。此项目使用Mattermost提供的一切功能保证火被一步步扑灭以及利益相关者被告知。"
},
{
"id": "worktemplate.devops.incident_resolution.description.channel",
"translation": "当到处都是问题的时候有一个能够确保一切都尽快回归正确的可重复流程是关键。此项目使用Mattermost提供的一切功能保证火被一步步扑灭以及利益相关者被告知。"
},
{
"id": "worktemplate.devops.incident_resolution.description.board",
"translation": "当到处都是问题的时候有一个能够确保一切都尽快回归正确的可重复流程是关键。此项目使用Mattermost提供的一切功能保证火被一步步扑灭以及利益相关者被告知。"
},
{
"id": "worktemplate.companywide.goals_and_okrs.integration",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.companywide.goals_and_okrs.channel",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.companywide.goals_and_okrs.board",
"translation": "清晰的目标对团队的成功至关重要在此项目里你可以在文档里写下团队的目标和OKR并在相关的频道里会有消息提醒。"
},
{
"id": "worktemplate.companywide.create_project.integration",
"translation": "使用此项目面板设计一个路线图,并在产生的频道里就相应话题进行探讨合作。"
},
{
"id": "worktemplate.companywide.create_project.channel",
"translation": "使用此项目面板设计一个路线图,并在产生的频道里就相应话题进行探讨合作。"
},
{
"id": "worktemplate.companywide.create_project.board",
"translation": "使用此项目面板设计一个路线图,并在产生的频道里就相应话题进行探讨合作。"
},
{
"id": "app.user.run.update_status.title",
"translation": "状态更新"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "更新状态"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "下次更新的提醒"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"other": "为利益相关者提供一次更新提醒。这条提醒将被广播到{{.Count}} 个频道。"
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "并且标记此运行为已结束"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "结束运行"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "对比上次存在更改"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} 启用了对 [{{.RunName}}]({{.RunURL}})的状态更新"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} 停止用了 [{{.RunName}}]({{.RunURL}})的状态更新。"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} 请求对 [{{.RunName}}]({{.RunURL}}) 进行状态更新。 \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} 是一个运行的参与者,并且希望要参加这个频道。任何的频道成员都可以邀请他们。\n"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "确认完成运行"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "完成运行"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"other": "一共有 **{{.Count}} 未完成的任务**. 您确定想为所有的参与者结束*{{.RunName}}*吗?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "添加到运行队列"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "时间表里显示的简单概要"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "最大64个字符"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "概要"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "添加到运行队列"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook运行"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "添加新任务"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "添加任务"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "名字"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "描述"
},
{
"id": "app.user.new_run.title",
"translation": "运行playbook"
},
{
"id": "app.user.new_run.submit_label",
"translation": "开始运行"
},
{
"id": "app.user.new_run.run_name",
"translation": "运行名"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.intro",
"translation": "**所有者** {{.Username}}"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "您没有任务。"
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"other": "您有 {{.Count}} 个任务现在已经逾期:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"other": "您一共有{{.Count}}个任务:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "您的任务"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "于昨天过期"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "{{.Count}}天已过期"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "将于今天过期"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"other": "{{.Count}}天后过期"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"other": "您有 **{{.Count}} 项目任务今天之后将要过期**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "请使用`/playbook todo`来查看您所有的任务。"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "您有0项正在运行。"
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"other": "您有{{.Count}}正在运行:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "正在运行"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "您没有逾期。"
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"other": "您有 {{.Count}} 过期需要状态更新:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "过期状态更新"
},
{
"id": "app.oauth.remove_auth_data_by_client_id.app_error",
"translation": "不能删除oauth认证信息。"
},
{
"id": "app.command.execute.error",
"translation": "无法执行命令。"
},
{
"id": "api.templates.license_up_for_renewal_contact_sales",
"translation": "联系销售"
},
{
"id": "api.license.true_up_review.not_allowed_for_cloud",
"translation": "云实例不允许真实性评估"
},
{
"id": "api.license.true_up_review.license_required",
"translation": "真实性评估需要许可证"
},
{
"id": "api.license.true_up_review.get_status_error",
"translation": "无法获取真实的状态记录"
},
{
"id": "api.license.true_up_review.create_error",
"translation": "无法创建真实的状态记录"
} }
] ]

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

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
@@ -53,12 +54,10 @@ const (
const ServerKey product.ServiceKey = "server" const ServerKey product.ServiceKey = "server"
// These credentials for Rudder need to be populated at build-time, // These credentials for Rudder need to be replaced at build-time.
// passing the following flags to the go build command: const (
// -ldflags "-X main.rudderDataplaneURL=<url> -X main.rudderWriteKey=<write_key>" rudderDataplaneURL = "placeholder_rudder_dataplane_url"
var ( rudderWriteKey = "placeholder_playbooks_rudder_key"
rudderDataplaneURL string
rudderWriteKey string
) )
var errServiceTypeAssert = errors.New("type assertion failed") var errServiceTypeAssert = errors.New("type assertion failed")
@@ -157,229 +156,9 @@ func newPlaybooksProduct(services map[product.ServiceKey]interface{}) (product.P
return nil, err return nil, err
} }
logger := logrus.StandardLogger()
ConfigureLogrus(logger, playbooks.logger)
playbooks.server = services[ServerKey].(*mmapp.Server) playbooks.server = services[ServerKey].(*mmapp.Server)
playbooks.serviceAdapter = newServiceAPIAdapter(playbooks) playbooks.serviceAdapter = newServiceAPIAdapter(playbooks)
botID, err := playbooks.serviceAdapter.EnsureBot(&model.Bot{
Username: "playbooks",
DisplayName: "Playbooks",
Description: "Playbooks bot.",
OwnerId: "playbooks",
})
if err != nil {
return nil, errors.Wrapf(err, "failed to ensure bot")
}
playbooks.config = config.NewConfigService(playbooks.serviceAdapter)
err = playbooks.config.UpdateConfiguration(func(c *config.Configuration) {
c.BotUserID = botID
c.AdminLogLevel = "debug"
})
if err != nil {
return nil, errors.Wrapf(err, "failed save bot to config")
}
playbooks.handler = api.NewHandler(playbooks.config)
if rudderDataplaneURL == "" || rudderWriteKey == "" {
logrus.Warn("Rudder credentials are not set. Disabling analytics.")
playbooks.telemetryClient = &telemetry.NoopTelemetry{}
} else {
diagnosticID := playbooks.serviceAdapter.GetDiagnosticID()
serverVersion := playbooks.serviceAdapter.GetServerVersion()
playbooks.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, model.BuildHashPlaybooks, serverVersion)
if err != nil {
return nil, errors.Wrapf(err, "failed init telemetry client")
}
}
toggleTelemetry := func() {
diagnosticsFlag := playbooks.serviceAdapter.GetConfig().LogSettings.EnableDiagnostics
telemetryEnabled := diagnosticsFlag != nil && *diagnosticsFlag
if telemetryEnabled {
if err = playbooks.telemetryClient.Enable(); err != nil {
logrus.WithError(err).Error("Telemetry could not be enabled")
}
return
}
if err = playbooks.telemetryClient.Disable(); err != nil {
logrus.WithError(err).Error("Telemetry could not be disabled")
}
}
toggleTelemetry()
playbooks.config.RegisterConfigChangeListener(toggleTelemetry)
apiClient := sqlstore.NewClient(playbooks.serviceAdapter)
playbooks.bot = bot.New(playbooks.serviceAdapter, playbooks.config.GetConfiguration().BotUserID, playbooks.config, playbooks.telemetryClient)
scheduler := cluster.GetJobOnceScheduler(playbooks.serviceAdapter)
sqlStore, err := sqlstore.New(apiClient, scheduler)
if err != nil {
return nil, errors.Wrapf(err, "failed creating the SQL store")
}
playbooks.playbookRunStore = sqlstore.NewPlaybookRunStore(apiClient, sqlStore)
playbooks.playbookStore = sqlstore.NewPlaybookStore(apiClient, sqlStore)
statsStore := sqlstore.NewStatsStore(apiClient, sqlStore)
playbooks.userInfoStore = sqlstore.NewUserInfoStore(sqlStore)
channelActionStore := sqlstore.NewChannelActionStore(apiClient, sqlStore)
categoryStore := sqlstore.NewCategoryStore(apiClient, sqlStore)
playbooks.handler = api.NewHandler(playbooks.config)
playbooks.playbookService = app.NewPlaybookService(playbooks.playbookStore, playbooks.bot, playbooks.telemetryClient, playbooks.serviceAdapter, playbooks.metricsService)
keywordsThreadIgnorer := app.NewKeywordsThreadIgnorer()
playbooks.channelActionService = app.NewChannelActionsService(playbooks.serviceAdapter, playbooks.bot, playbooks.config, channelActionStore, playbooks.playbookService, keywordsThreadIgnorer, playbooks.telemetryClient)
playbooks.categoryService = app.NewCategoryService(categoryStore, playbooks.serviceAdapter, playbooks.telemetryClient)
playbooks.licenseChecker = enterprise.NewLicenseChecker(playbooks.serviceAdapter)
playbooks.playbookRunService = app.NewPlaybookRunService(
playbooks.playbookRunStore,
playbooks.bot,
playbooks.config,
scheduler,
playbooks.telemetryClient,
playbooks.telemetryClient,
playbooks.serviceAdapter,
playbooks.playbookService,
playbooks.channelActionService,
playbooks.licenseChecker,
playbooks.metricsService,
)
if err = scheduler.SetCallback(playbooks.playbookRunService.HandleReminder); err != nil {
logrus.WithError(err).Error("JobOnceScheduler could not add the playbookRunService's HandleReminder")
}
if err = scheduler.Start(); err != nil {
logrus.WithError(err).Error("JobOnceScheduler could not start")
}
// Migrations use the scheduler, so they have to be run after playbookRunService and scheduler have started
mutex, err := cluster.NewMutex(playbooks.serviceAdapter, "IR_dbMutex")
if err != nil {
return nil, errors.Wrapf(err, "failed creating cluster mutex")
}
mutex.Lock()
if err = sqlStore.RunMigrations(); err != nil {
mutex.Unlock()
return nil, errors.Wrapf(err, "failed to run migrations")
}
mutex.Unlock()
playbooks.permissions = app.NewPermissionsService(
playbooks.playbookService,
playbooks.playbookRunService,
playbooks.serviceAdapter,
playbooks.config,
playbooks.licenseChecker,
)
// register collections and topics.
// TODO bump the minimum server version
if err = playbooks.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeStatus); err != nil {
logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeStatus).Warnf("failed to register collection and topic")
}
if err = playbooks.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeTask); err != nil {
logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeTask).Warnf("failed to register collection and topic")
}
api.NewGraphQLHandler(
playbooks.handler.APIRouter,
playbooks.playbookService,
playbooks.playbookRunService,
playbooks.categoryService,
playbooks.serviceAdapter,
playbooks.config,
playbooks.permissions,
playbooks.playbookStore,
playbooks.licenseChecker,
)
api.NewPlaybookHandler(
playbooks.handler.APIRouter,
playbooks.playbookService,
playbooks.serviceAdapter,
playbooks.config,
playbooks.permissions,
)
api.NewPlaybookRunHandler(
playbooks.handler.APIRouter,
playbooks.playbookRunService,
playbooks.playbookService,
playbooks.permissions,
playbooks.licenseChecker,
playbooks.serviceAdapter,
playbooks.bot,
playbooks.config,
)
api.NewStatsHandler(
playbooks.handler.APIRouter,
playbooks.serviceAdapter,
statsStore,
playbooks.playbookService,
playbooks.permissions,
playbooks.licenseChecker,
)
api.NewBotHandler(
playbooks.handler.APIRouter,
playbooks.serviceAdapter, playbooks.bot,
playbooks.config,
playbooks.playbookRunService,
playbooks.userInfoStore,
)
api.NewTelemetryHandler(
playbooks.handler.APIRouter,
playbooks.playbookRunService,
playbooks.serviceAdapter,
playbooks.telemetryClient,
playbooks.playbookService,
playbooks.telemetryClient,
playbooks.telemetryClient,
playbooks.telemetryClient,
playbooks.permissions,
)
api.NewSignalHandler(
playbooks.handler.APIRouter,
playbooks.serviceAdapter,
playbooks.playbookRunService,
playbooks.playbookService,
keywordsThreadIgnorer,
)
api.NewSettingsHandler(
playbooks.handler.APIRouter,
playbooks.serviceAdapter,
playbooks.config,
)
api.NewActionsHandler(
playbooks.handler.APIRouter,
playbooks.channelActionService,
playbooks.serviceAdapter,
playbooks.permissions,
)
api.NewCategoryHandler(
playbooks.handler.APIRouter,
playbooks.serviceAdapter,
playbooks.categoryService,
playbooks.playbookService,
playbooks.playbookRunService,
)
isTestingEnabled := false
flag := playbooks.serviceAdapter.GetConfig().ServiceSettings.EnableTesting
if flag != nil {
isTestingEnabled = *flag
}
if err = command.RegisterCommands(playbooks.serviceAdapter.RegisterCommand, isTestingEnabled); err != nil {
return nil, errors.Wrapf(err, "failed register commands")
}
return playbooks, nil return playbooks, nil
} }
@@ -531,6 +310,228 @@ func (pp *playbooksProduct) setProductServices(services map[product.ServiceKey]i
} }
func (pp *playbooksProduct) Start() error { func (pp *playbooksProduct) Start() error {
logger := logrus.StandardLogger()
ConfigureLogrus(logger, pp.logger)
botID, err := pp.serviceAdapter.EnsureBot(&model.Bot{
Username: "playbooks",
DisplayName: "Playbooks",
Description: "Playbooks bot.",
OwnerId: "playbooks",
})
if err != nil {
return errors.Wrapf(err, "failed to ensure bot")
}
pp.config = config.NewConfigService(pp.serviceAdapter)
err = pp.config.UpdateConfiguration(func(c *config.Configuration) {
c.BotUserID = botID
c.AdminLogLevel = "debug"
})
if err != nil {
return errors.Wrapf(err, "failed save bot to config")
}
pp.handler = api.NewHandler(pp.config)
if strings.HasPrefix(rudderWriteKey, "placeholder_") {
logrus.Warn("Rudder credentials are not set. Disabling analytics.")
pp.telemetryClient = &telemetry.NoopTelemetry{}
} else {
logrus.Info("Rudder credentials are set. Enabling analytics.")
diagnosticID := pp.serviceAdapter.GetDiagnosticID()
serverVersion := pp.serviceAdapter.GetServerVersion()
pp.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, model.BuildHashPlaybooks, serverVersion)
if err != nil {
return errors.Wrapf(err, "failed init telemetry client")
}
}
toggleTelemetry := func() {
diagnosticsFlag := pp.serviceAdapter.GetConfig().LogSettings.EnableDiagnostics
telemetryEnabled := diagnosticsFlag != nil && *diagnosticsFlag
if telemetryEnabled {
if err = pp.telemetryClient.Enable(); err != nil {
logrus.WithError(err).Error("Telemetry could not be enabled")
}
return
}
if err = pp.telemetryClient.Disable(); err != nil {
logrus.WithError(err).Error("Telemetry could not be disabled")
}
}
toggleTelemetry()
pp.config.RegisterConfigChangeListener(toggleTelemetry)
apiClient := sqlstore.NewClient(pp.serviceAdapter)
pp.bot = bot.New(pp.serviceAdapter, pp.config.GetConfiguration().BotUserID, pp.config, pp.telemetryClient)
scheduler := cluster.GetJobOnceScheduler(pp.serviceAdapter)
sqlStore, err := sqlstore.New(apiClient, scheduler)
if err != nil {
return errors.Wrapf(err, "failed creating the SQL store")
}
pp.playbookRunStore = sqlstore.NewPlaybookRunStore(apiClient, sqlStore)
pp.playbookStore = sqlstore.NewPlaybookStore(apiClient, sqlStore)
statsStore := sqlstore.NewStatsStore(apiClient, sqlStore)
pp.userInfoStore = sqlstore.NewUserInfoStore(sqlStore)
channelActionStore := sqlstore.NewChannelActionStore(apiClient, sqlStore)
categoryStore := sqlstore.NewCategoryStore(apiClient, sqlStore)
pp.handler = api.NewHandler(pp.config)
pp.playbookService = app.NewPlaybookService(pp.playbookStore, pp.bot, pp.telemetryClient, pp.serviceAdapter, pp.metricsService)
keywordsThreadIgnorer := app.NewKeywordsThreadIgnorer()
pp.channelActionService = app.NewChannelActionsService(pp.serviceAdapter, pp.bot, pp.config, channelActionStore, pp.playbookService, keywordsThreadIgnorer, pp.telemetryClient)
pp.categoryService = app.NewCategoryService(categoryStore, pp.serviceAdapter, pp.telemetryClient)
pp.licenseChecker = enterprise.NewLicenseChecker(pp.serviceAdapter)
pp.playbookRunService = app.NewPlaybookRunService(
pp.playbookRunStore,
pp.bot,
pp.config,
scheduler,
pp.telemetryClient,
pp.telemetryClient,
pp.serviceAdapter,
pp.playbookService,
pp.channelActionService,
pp.licenseChecker,
pp.metricsService,
)
if err = scheduler.SetCallback(pp.playbookRunService.HandleReminder); err != nil {
logrus.WithError(err).Error("JobOnceScheduler could not add the playbookRunService's HandleReminder")
}
if err = scheduler.Start(); err != nil {
logrus.WithError(err).Error("JobOnceScheduler could not start")
}
// Migrations use the scheduler, so they have to be run after playbookRunService and scheduler have started
mutex, err := cluster.NewMutex(pp.serviceAdapter, "IR_dbMutex")
if err != nil {
return errors.Wrapf(err, "failed creating cluster mutex")
}
mutex.Lock()
if err = sqlStore.RunMigrations(); err != nil {
mutex.Unlock()
return errors.Wrapf(err, "failed to run migrations")
}
mutex.Unlock()
pp.permissions = app.NewPermissionsService(
pp.playbookService,
pp.playbookRunService,
pp.serviceAdapter,
pp.config,
pp.licenseChecker,
)
// register collections and topics.
// TODO bump the minimum server version
if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeStatus); err != nil {
logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeStatus).Warnf("failed to register collection and topic")
}
if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeTask); err != nil {
logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeTask).Warnf("failed to register collection and topic")
}
api.NewGraphQLHandler(
pp.handler.APIRouter,
pp.playbookService,
pp.playbookRunService,
pp.categoryService,
pp.serviceAdapter,
pp.config,
pp.permissions,
pp.playbookStore,
pp.licenseChecker,
)
api.NewPlaybookHandler(
pp.handler.APIRouter,
pp.playbookService,
pp.serviceAdapter,
pp.config,
pp.permissions,
)
api.NewPlaybookRunHandler(
pp.handler.APIRouter,
pp.playbookRunService,
pp.playbookService,
pp.permissions,
pp.licenseChecker,
pp.serviceAdapter,
pp.bot,
pp.config,
)
api.NewStatsHandler(
pp.handler.APIRouter,
pp.serviceAdapter,
statsStore,
pp.playbookService,
pp.permissions,
pp.licenseChecker,
)
api.NewBotHandler(
pp.handler.APIRouter,
pp.serviceAdapter, pp.bot,
pp.config,
pp.playbookRunService,
pp.userInfoStore,
)
api.NewTelemetryHandler(
pp.handler.APIRouter,
pp.playbookRunService,
pp.serviceAdapter,
pp.telemetryClient,
pp.playbookService,
pp.telemetryClient,
pp.telemetryClient,
pp.telemetryClient,
pp.permissions,
)
api.NewSignalHandler(
pp.handler.APIRouter,
pp.serviceAdapter,
pp.playbookRunService,
pp.playbookService,
keywordsThreadIgnorer,
)
api.NewSettingsHandler(
pp.handler.APIRouter,
pp.serviceAdapter,
pp.config,
)
api.NewActionsHandler(
pp.handler.APIRouter,
pp.channelActionService,
pp.serviceAdapter,
pp.permissions,
)
api.NewCategoryHandler(
pp.handler.APIRouter,
pp.serviceAdapter,
pp.categoryService,
pp.playbookService,
pp.playbookRunService,
)
isTestingEnabled := false
flag := pp.serviceAdapter.GetConfig().ServiceSettings.EnableTesting
if flag != nil {
isTestingEnabled = *flag
}
if err = command.RegisterCommands(pp.serviceAdapter.RegisterCommand, isTestingEnabled); err != nil {
return errors.Wrapf(err, "failed register commands")
}
if err := pp.hooksService.RegisterHooks(playbooksProductName, pp); err != nil { if err := pp.hooksService.RegisterHooks(playbooksProductName, pp); err != nil {
return fmt.Errorf("failed to register hooks: %w", err) return fmt.Errorf("failed to register hooks: %w", err)
} }

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

@@ -15,8 +15,7 @@ import (
) )
func TestActionCreation(t *testing.T) { func TestActionCreation(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
createNewChannel := func(t *testing.T, name string) *model.Channel { createNewChannel := func(t *testing.T, name string) *model.Channel {
@@ -201,8 +200,7 @@ func TestActionCreation(t *testing.T) {
} }
func TestActionList(t *testing.T) { func TestActionList(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
// Create three valid actions // Create three valid actions
@@ -294,8 +292,7 @@ func TestActionList(t *testing.T) {
} }
func TestActionUpdate(t *testing.T) { func TestActionUpdate(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
// Create a valid action // Create a valid action

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

@@ -16,8 +16,7 @@ func TestTrialLicences(t *testing.T) {
// This test is flaky due to upstream connectivity issues. // This test is flaky due to upstream connectivity issues.
t.Skip() t.Skip()
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("request trial license without permissions", func(t *testing.T) { t.Run("request trial license without permissions", func(t *testing.T) {

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

@@ -11,8 +11,7 @@ import (
) )
func TestAPI(t *testing.T) { func TestAPI(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateClients() e.CreateClients()
t.Run("404", func(t *testing.T) { t.Run("404", func(t *testing.T) {

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

@@ -21,8 +21,7 @@ import (
) )
func TestGraphQLPlaybooks(t *testing.T) { func TestGraphQLPlaybooks(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("basic get", func(t *testing.T) { t.Run("basic get", func(t *testing.T) {
@@ -206,8 +205,7 @@ func TestGraphQLPlaybooks(t *testing.T) {
} }
func TestGraphQLUpdatePlaybookFails(t *testing.T) { func TestGraphQLUpdatePlaybookFails(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("update playbook fails because size constraints.", func(t *testing.T) { t.Run("update playbook fails because size constraints.", func(t *testing.T) {
@@ -370,8 +368,7 @@ func TestGraphQLUpdatePlaybookFails(t *testing.T) {
} }
func TestUpdatePlaybookFavorite(t *testing.T) { func TestUpdatePlaybookFavorite(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("favorite", func(t *testing.T) { t.Run("favorite", func(t *testing.T) {
@@ -493,8 +490,7 @@ func gqlTestPlaybookUpdate(e *TestEnvironment, t *testing.T, playbookID string,
} }
func TestGraphQLPlaybooksMetrics(t *testing.T) { func TestGraphQLPlaybooksMetrics(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("metrics get", func(t *testing.T) { t.Run("metrics get", func(t *testing.T) {

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

@@ -20,8 +20,7 @@ import (
) )
func TestGraphQLRunList(t *testing.T) { func TestGraphQLRunList(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("list by participantOrFollower", func(t *testing.T) { t.Run("list by participantOrFollower", func(t *testing.T) {
@@ -206,8 +205,7 @@ func TestGraphQLRunList(t *testing.T) {
} }
func TestGraphQLChangeRunParticipants(t *testing.T) { func TestGraphQLChangeRunParticipants(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
user3, _, err := e.ServerAdminClient.CreateUser(&model.User{ user3, _, err := e.ServerAdminClient.CreateUser(&model.User{
@@ -669,8 +667,7 @@ func TestGraphQLChangeRunParticipants(t *testing.T) {
} }
func TestGraphQLChangeRunOwner(t *testing.T) { func TestGraphQLChangeRunOwner(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
// create a third user to test change owner // create a third user to test change owner
@@ -713,8 +710,7 @@ func TestGraphQLChangeRunOwner(t *testing.T) {
} }
func TestSetRunFavorite(t *testing.T) { func TestSetRunFavorite(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
createRun := func() *client.PlaybookRun { createRun := func() *client.PlaybookRun {
@@ -800,8 +796,7 @@ func TestSetRunFavorite(t *testing.T) {
} }
func TestResolverFavorites(t *testing.T) { func TestResolverFavorites(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
createRun := func() *client.PlaybookRun { createRun := func() *client.PlaybookRun {
@@ -833,8 +828,7 @@ func TestResolverFavorites(t *testing.T) {
} }
func TestResolverPlaybooks(t *testing.T) { func TestResolverPlaybooks(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
createRun := func() *client.PlaybookRun { createRun := func() *client.PlaybookRun {
@@ -860,8 +854,7 @@ func TestResolverPlaybooks(t *testing.T) {
} }
func TestUpdateRun(t *testing.T) { func TestUpdateRun(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
createRun := func() *client.PlaybookRun { createRun := func() *client.PlaybookRun {
@@ -977,8 +970,7 @@ func TestUpdateRun(t *testing.T) {
} }
func TestUpdateRunTaskActions(t *testing.T) { func TestUpdateRunTaskActions(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("task actions mutation create and update", func(t *testing.T) { t.Run("task actions mutation create and update", func(t *testing.T) {
@@ -1071,8 +1063,7 @@ func TestUpdateRunTaskActions(t *testing.T) {
} }
func TestBadGraphQLRequest(t *testing.T) { func TestBadGraphQLRequest(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
testRunsQuery := ` testRunsQuery := `

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

@@ -22,8 +22,7 @@ import (
) )
func TestPlaybooks(t *testing.T) { func TestPlaybooks(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateClients() e.CreateClients()
e.CreateBasicServer() e.CreateBasicServer()
@@ -267,8 +266,7 @@ func TestPlaybooks(t *testing.T) {
} }
func TestCreateInvalidPlaybook(t *testing.T) { func TestCreateInvalidPlaybook(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateClients() e.CreateClients()
e.CreateBasicServer() e.CreateBasicServer()
@@ -369,8 +367,7 @@ func TestCreateInvalidPlaybook(t *testing.T) {
} }
func TestPlaybooksRetrieval(t *testing.T) { func TestPlaybooksRetrieval(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("get playbook", func(t *testing.T) { t.Run("get playbook", func(t *testing.T) {
@@ -387,8 +384,7 @@ func TestPlaybooksRetrieval(t *testing.T) {
} }
func TestPlaybookUpdate(t *testing.T) { func TestPlaybookUpdate(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("update playbook properties", func(t *testing.T) { t.Run("update playbook properties", func(t *testing.T) {
@@ -521,8 +517,7 @@ func TestPlaybookUpdate(t *testing.T) {
} }
func TestPlaybookUpdateCrossTeam(t *testing.T) { func TestPlaybookUpdateCrossTeam(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("update playbook properties not in team public playbook", func(t *testing.T) { t.Run("update playbook properties not in team public playbook", func(t *testing.T) {
@@ -552,8 +547,7 @@ func TestPlaybookUpdateCrossTeam(t *testing.T) {
} }
func TestPlaybooksSort(t *testing.T) { func TestPlaybooksSort(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateClients() e.CreateClients()
e.CreateBasicServer() e.CreateBasicServer()
e.SetE20Licence() e.SetE20Licence()
@@ -795,8 +789,7 @@ func TestPlaybooksSort(t *testing.T) {
} }
func TestPlaybooksPaging(t *testing.T) { func TestPlaybooksPaging(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateClients() e.CreateClients()
e.CreateBasicServer() e.CreateBasicServer()
e.SetE20Licence() e.SetE20Licence()
@@ -935,8 +928,7 @@ func getPlaybookIDsList(playbooks []client.Playbook) []string {
} }
func TestPlaybooksPermissions(t *testing.T) { func TestPlaybooksPermissions(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("test no permissions to create", func(t *testing.T) { t.Run("test no permissions to create", func(t *testing.T) {
@@ -1148,8 +1140,7 @@ func TestPlaybooksPermissions(t *testing.T) {
} }
func TestPlaybooksConversions(t *testing.T) { func TestPlaybooksConversions(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("public to private conversion", func(t *testing.T) { t.Run("public to private conversion", func(t *testing.T) {
@@ -1208,8 +1199,7 @@ func TestPlaybooksConversions(t *testing.T) {
} }
func TestPlaybooksImportExport(t *testing.T) { func TestPlaybooksImportExport(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateClients() e.CreateClients()
e.CreateBasicServer() e.CreateBasicServer()
e.CreateBasicPublicPlaybook() e.CreateBasicPublicPlaybook()
@@ -1237,8 +1227,7 @@ func TestPlaybooksImportExport(t *testing.T) {
} }
func TestPlaybooksDuplicate(t *testing.T) { func TestPlaybooksDuplicate(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateClients() e.CreateClients()
e.CreateBasicServer() e.CreateBasicServer()
e.SetE20Licence() e.SetE20Licence()
@@ -1259,8 +1248,7 @@ func TestPlaybooksDuplicate(t *testing.T) {
} }
func TestAddPostToTimeline(t *testing.T) { func TestAddPostToTimeline(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
dialogRequest := model.SubmitDialogRequest{ dialogRequest := model.SubmitDialogRequest{
@@ -1307,8 +1295,7 @@ func TestAddPostToTimeline(t *testing.T) {
} }
func TestPlaybookStats(t *testing.T) { func TestPlaybookStats(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateClients() e.CreateClients()
e.CreateBasicServer() e.CreateBasicServer()
e.SetE20Licence() e.SetE20Licence()
@@ -1343,8 +1330,7 @@ func TestPlaybookStats(t *testing.T) {
} }
func TestPlaybookGetAutoFollows(t *testing.T) { func TestPlaybookGetAutoFollows(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
p1ID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ p1ID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{
@@ -1450,8 +1436,7 @@ func TestPlaybookGetAutoFollows(t *testing.T) {
} }
func TestPlaybookChecklistCleanup(t *testing.T) { func TestPlaybookChecklistCleanup(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("update playbook", func(t *testing.T) { t.Run("update playbook", func(t *testing.T) {

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

@@ -19,8 +19,7 @@ import (
) )
func TestRunCreation(t *testing.T) { func TestRunCreation(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
incompletePlaybookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ incompletePlaybookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{
@@ -314,8 +313,7 @@ func TestRunCreation(t *testing.T) {
} }
func TestCreateRunInExistingChannel(t *testing.T) { func TestCreateRunInExistingChannel(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
// create playbook // create playbook
@@ -410,8 +408,7 @@ func TestCreateRunInExistingChannel(t *testing.T) {
} }
func TestCreateInvalidRuns(t *testing.T) { func TestCreateInvalidRuns(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("fails if description is longer than 4096", func(t *testing.T) { t.Run("fails if description is longer than 4096", func(t *testing.T) {
@@ -428,8 +425,7 @@ func TestCreateInvalidRuns(t *testing.T) {
} }
func TestRunRetrieval(t *testing.T) { func TestRunRetrieval(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("by channel id", func(t *testing.T) { t.Run("by channel id", func(t *testing.T) {
@@ -510,8 +506,7 @@ func TestRunRetrieval(t *testing.T) {
} }
func TestRunPostStatusUpdate(t *testing.T) { func TestRunPostStatusUpdate(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("post an update", func(t *testing.T) { t.Run("post an update", func(t *testing.T) {
@@ -571,8 +566,7 @@ func TestRunPostStatusUpdate(t *testing.T) {
} }
func TestChecklistManagement(t *testing.T) { func TestChecklistManagement(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
createNewRunWithNoChecklists := func(t *testing.T) *client.PlaybookRun { createNewRunWithNoChecklists := func(t *testing.T) *client.PlaybookRun {
@@ -1188,8 +1182,7 @@ func TestChecklistManagement(t *testing.T) {
} }
func TestChecklisFailTooLarge(t *testing.T) { func TestChecklisFailTooLarge(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("checklist creation - failure: too large checklist", func(t *testing.T) { t.Run("checklist creation - failure: too large checklist", func(t *testing.T) {
@@ -1213,8 +1206,7 @@ func TestChecklisFailTooLarge(t *testing.T) {
} }
func TestRunGetStatusUpdates(t *testing.T) { func TestRunGetStatusUpdates(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("public - get no updates", func(t *testing.T) { t.Run("public - get no updates", func(t *testing.T) {
@@ -1343,8 +1335,7 @@ func TestRunGetStatusUpdates(t *testing.T) {
} }
func TestRequestUpdate(t *testing.T) { func TestRequestUpdate(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("private - no viewer access ", func(t *testing.T) { t.Run("private - no viewer access ", func(t *testing.T) {
@@ -1437,8 +1428,7 @@ func TestRequestUpdate(t *testing.T) {
} }
func TestReminderReset(t *testing.T) { func TestReminderReset(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("reminder reset - timeline event created", func(t *testing.T) { t.Run("reminder reset - timeline event created", func(t *testing.T) {
@@ -1485,8 +1475,7 @@ func TestReminderReset(t *testing.T) {
} }
func TestChecklisItem_SetAssignee(t *testing.T) { func TestChecklisItem_SetAssignee(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
addSimpleChecklistToTun := func(t *testing.T, runID string) *client.PlaybookRun { addSimpleChecklistToTun := func(t *testing.T, runID string) *client.PlaybookRun {
@@ -1597,8 +1586,7 @@ func TestChecklisItem_SetAssignee(t *testing.T) {
} }
func TestChecklisItem_SetCommand(t *testing.T) { func TestChecklisItem_SetCommand(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{
@@ -1699,8 +1687,7 @@ func TestChecklisItem_SetCommand(t *testing.T) {
} }
func TestGetOwners(t *testing.T) { func TestGetOwners(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
ownerFromUser := func(u *model.User) client.OwnerInfo { ownerFromUser := func(u *model.User) client.OwnerInfo {

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

@@ -14,8 +14,7 @@ import (
) )
func TestSettings(t *testing.T) { func TestSettings(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("get settings", func(t *testing.T) { t.Run("get settings", func(t *testing.T) {

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

@@ -16,8 +16,7 @@ import (
) )
func TestGetSiteStats(t *testing.T) { func TestGetSiteStats(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("get sites stats", func(t *testing.T) { t.Run("get sites stats", func(t *testing.T) {
@@ -50,8 +49,7 @@ func TestGetSiteStats(t *testing.T) {
} }
func TestPlaybookKeyMetricsStats(t *testing.T) { func TestPlaybookKeyMetricsStats(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("3 runs with published metrics, 2 runs without publishing", func(t *testing.T) { t.Run("3 runs with published metrics, 2 runs without publishing", func(t *testing.T) {

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

@@ -11,8 +11,7 @@ import (
) )
func TestCreateEvent(t *testing.T) { func TestCreateEvent(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
t.Run("create an event with bad type fails", func(t *testing.T) { t.Run("create an event with bad type fails", func(t *testing.T) {

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

@@ -97,7 +97,7 @@ func getEnvWithDefault(name, defaultValue string) string {
return defaultValue return defaultValue
} }
func Setup(t *testing.T) (*TestEnvironment, func()) { func Setup(t *testing.T) *TestEnvironment {
// Ignore any locally defined SiteURL as we intend to host our own. // Ignore any locally defined SiteURL as we intend to host our own.
os.Unsetenv("MM_SERVICESETTINGS_SITEURL") os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
os.Unsetenv("MM_SERVICESETTINGS_LISTENADDRESS") os.Unsetenv("MM_SERVICESETTINGS_LISTENADDRESS")
@@ -126,11 +126,6 @@ func Setup(t *testing.T) (*TestEnvironment, func()) {
config.LogSettings.EnableFile = model.NewBool(false) config.LogSettings.EnableFile = model.NewBool(false)
config.LogSettings.ConsoleLevel = model.NewString("INFO") config.LogSettings.ConsoleLevel = model.NewString("INFO")
// disable Boards through the feature flag
boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct")
os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct")
config.FeatureFlags.BoardsProduct = false
// override config with e2etest.config.json if it exists // override config with e2etest.config.json if it exists
textConfig, err := os.ReadFile("./e2etest.config.json") textConfig, err := os.ReadFile("./e2etest.config.json")
if err == nil { if err == nil {
@@ -169,10 +164,6 @@ func Setup(t *testing.T) (*TestEnvironment, func()) {
ap := sapp.New(sapp.ServerConnector(server.Channels())) ap := sapp.New(sapp.ServerConnector(server.Channels()))
teardown := func() {
os.Setenv("MM_FEATUREFLAGS_BoardsProduct", boardsProductEnvValue)
}
return &TestEnvironment{ return &TestEnvironment{
T: t, T: t,
Srv: server, Srv: server,
@@ -184,7 +175,7 @@ func Setup(t *testing.T) (*TestEnvironment, func()) {
}, },
}, },
logger: testLogger, logger: testLogger,
}, teardown }
} }
func (e *TestEnvironment) CreateClients() { func (e *TestEnvironment) CreateClients() {
@@ -478,8 +469,7 @@ func (e *TestEnvironment) CreateBasic() {
// TestTestFramework If this is failing you know the break is not exclusively in your test. // TestTestFramework If this is failing you know the break is not exclusively in your test.
func TestTestFramework(t *testing.T) { func TestTestFramework(t *testing.T) {
e, teardown := Setup(t) e := Setup(t)
defer teardown()
e.CreateBasic() e.CreateBasic()
} }

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

@@ -85,7 +85,7 @@ function getSubpath(siteURL: string): string {
return url.pathname.replace(/\/+$/, '') return url.pathname.replace(/\/+$/, '')
} }
const TELEMETRY_RUDDER_KEY = 'placeholder_rudder_key' const TELEMETRY_RUDDER_KEY = 'placeholder_boards_rudder_key'
const TELEMETRY_RUDDER_DATAPLANE_URL = 'placeholder_rudder_dataplane_url' const TELEMETRY_RUDDER_DATAPLANE_URL = 'placeholder_rudder_dataplane_url'
const TELEMETRY_OPTIONS = { const TELEMETRY_OPTIONS = {
context: { context: {

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

@@ -2497,7 +2497,11 @@ const AdminDefinition = {
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.NOTIFICATIONS)), it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.NOTIFICATIONS)),
it.stateIsFalse('EmailSettings.SendEmailNotifications'), it.stateIsFalse('EmailSettings.SendEmailNotifications'),
), ),
validate: validators.isRequired(t('admin.environment.notifications.feedbackEmail.required'), '"Notification From Address" is required'),
// MM-50952
// If the setting is hidden, then it is not being set in state so there is
// nothing to validate, and validation would fail anyways and prevent saving
validate: it.configIsFalse('ExperimentalSettings', 'RestrictSystemAdmin') && validators.isRequired(t('admin.environment.notifications.feedbackEmail.required'), '"Notification From Address" is required'),
}, },
{ {
type: Constants.SettingsTypes.TYPE_TEXT, type: Constants.SettingsTypes.TYPE_TEXT,

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

@@ -5,16 +5,12 @@ import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
type Props = { const CancelSubscription = () => {
cancelAccountLink: any; const description = `I am requesting that workspace "${window.location.host}" be deleted`;
} const [, contactSupportURL] = useOpenCloudZendeskSupportForm('Request workspace be deleted', description);
const CancelSubscription = (props: Props) => {
const {
cancelAccountLink,
} = props;
return ( return (
<div className='cancelSubscriptionSection'> <div className='cancelSubscriptionSection'>
@@ -33,7 +29,7 @@ const CancelSubscription = (props: Props) => {
</div> </div>
<ExternalLink <ExternalLink
location='cancel_subscription' location='cancel_subscription'
href={cancelAccountLink} href={contactSupportURL}
className='cancelSubscriptionSection__contactUs' className='cancelSubscriptionSection__contactUs'
onClick={() => trackEvent('cloud_admin', 'click_contact_us')} onClick={() => trackEvent('cloud_admin', 'click_contact_us')}
> >

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

@@ -24,7 +24,6 @@ import AlertBanner from 'components/alert_banner';
import UpgradeLink from 'components/widgets/links/upgrade_link'; import UpgradeLink from 'components/widgets/links/upgrade_link';
import './cloud_trial_banner.scss'; import './cloud_trial_banner.scss';
import {SalesInquiryIssue} from 'selectors/cloud';
export interface Props { export interface Props {
trialEndDate: number; trialEndDate: number;
@@ -34,7 +33,7 @@ const CloudTrialBanner = ({trialEndDate}: Props): JSX.Element | null => {
const endDate = new Date(trialEndDate); const endDate = new Date(trialEndDate);
const DISMISSED_DAYS = 10; const DISMISSED_DAYS = 10;
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); const [openSalesLink] = useOpenSalesLink();
const dispatch = useDispatch(); const dispatch = useDispatch();
const user = useSelector(getCurrentUser); const user = useSelector(getCurrentUser);
const storedDismissedEndDate = useSelector((state: GlobalState) => getPreference(state, Preferences.CLOUD_TRIAL_BANNER, CloudBanners.UPGRADE_FROM_TRIAL)); const storedDismissedEndDate = useSelector((state: GlobalState) => getPreference(state, Preferences.CLOUD_TRIAL_BANNER, CloudBanners.UPGRADE_FROM_TRIAL));

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

@@ -10,21 +10,19 @@ import {CloudLinks, CloudProducts} from 'utils/constants';
import PrivateCloudSvg from 'components/common/svg_images_components/private_cloud_svg'; import PrivateCloudSvg from 'components/common/svg_images_components/private_cloud_svg';
import CloudTrialSvg from 'components/common/svg_images_components/cloud_trial_svg'; import CloudTrialSvg from 'components/common/svg_images_components/cloud_trial_svg';
import {TelemetryProps} from 'components/common/hooks/useOpenPricingModal'; import {TelemetryProps} from 'components/common/hooks/useOpenPricingModal';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
type Props = { type Props = {
contactSalesLink: any;
isFreeTrial: boolean; isFreeTrial: boolean;
trialQuestionsLink: any;
subscriptionPlan: string | undefined; subscriptionPlan: string | undefined;
onUpgradeMattermostCloud: (telemetryProps?: TelemetryProps | undefined) => void; onUpgradeMattermostCloud: (telemetryProps?: TelemetryProps | undefined) => void;
} }
const ContactSalesCard = (props: Props) => { const ContactSalesCard = (props: Props) => {
const [openSalesLink, contactSalesLink] = useOpenSalesLink();
const { const {
contactSalesLink,
isFreeTrial, isFreeTrial,
trialQuestionsLink,
subscriptionPlan, subscriptionPlan,
onUpgradeMattermostCloud, onUpgradeMattermostCloud,
} = props; } = props;
@@ -145,7 +143,7 @@ const ContactSalesCard = (props: Props) => {
{(isFreeTrial || subscriptionPlan === CloudProducts.ENTERPRISE || isCloudLegacyPlan) && {(isFreeTrial || subscriptionPlan === CloudProducts.ENTERPRISE || isCloudLegacyPlan) &&
<ExternalLink <ExternalLink
location='contact_sales_card' location='contact_sales_card'
href={isFreeTrial ? trialQuestionsLink : contactSalesLink} href={contactSalesLink}
className='PrivateCloudCard__actionButton' className='PrivateCloudCard__actionButton'
onClick={() => trackEvent('cloud_admin', 'click_contact_sales')} onClick={() => trackEvent('cloud_admin', 'click_contact_sales')}
> >
@@ -163,7 +161,7 @@ const ContactSalesCard = (props: Props) => {
if (subscriptionPlan === CloudProducts.STARTER) { if (subscriptionPlan === CloudProducts.STARTER) {
onUpgradeMattermostCloud({trackingLocation: 'admin_console_subscription_card_upgrade_now_button'}); onUpgradeMattermostCloud({trackingLocation: 'admin_console_subscription_card_upgrade_now_button'});
} else { } else {
window.open(contactSalesLink, '_blank'); openSalesLink();
} }
}} }}
className='PrivateCloudCard__actionButton' className='PrivateCloudCard__actionButton'

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

@@ -13,7 +13,6 @@ import FormattedAdminHeader from 'components/widgets/admin_console/formatted_adm
import CloudTrialBanner from 'components/admin_console/billing/billing_subscriptions/cloud_trial_banner'; import CloudTrialBanner from 'components/admin_console/billing/billing_subscriptions/cloud_trial_banner';
import CloudFetchError from 'components/cloud_fetch_error'; import CloudFetchError from 'components/cloud_fetch_error';
import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud';
import { import {
getSubscriptionProduct, getSubscriptionProduct,
getCloudSubscription as selectCloudSubscription, getCloudSubscription as selectCloudSubscription,
@@ -63,9 +62,6 @@ const BillingSubscriptions = () => {
const isCardExpired = isCustomerCardExpired(useSelector(selectCloudCustomer)); const isCardExpired = isCustomerCardExpired(useSelector(selectCloudCustomer));
const contactSalesLink = useSelector(getCloudContactUsLink)(InquiryType.Sales);
const cancelAccountLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.CancelAccount);
const trialQuestionsLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.TrialQuestions);
const trialEndDate = subscription?.trial_end_at || 0; const trialEndDate = subscription?.trial_end_at || 0;
const [showCreditCardBanner, setShowCreditCardBanner] = useState(true); const [showCreditCardBanner, setShowCreditCardBanner] = useState(true);
@@ -159,19 +155,12 @@ const BillingSubscriptions = () => {
<Limits/> <Limits/>
) : ( ) : (
<ContactSalesCard <ContactSalesCard
contactSalesLink={contactSalesLink}
isFreeTrial={isFreeTrial} isFreeTrial={isFreeTrial}
trialQuestionsLink={trialQuestionsLink}
subscriptionPlan={product?.sku} subscriptionPlan={product?.sku}
onUpgradeMattermostCloud={openPricingModal} onUpgradeMattermostCloud={openPricingModal}
/> />
)} )}
{isAnnualProfessionalOrEnterprise && !isFreeTrial ? {isAnnualProfessionalOrEnterprise && !isFreeTrial ? <CancelSubscription/> : <DeleteWorkspaceCTA/>}
<CancelSubscription
cancelAccountLink={cancelAccountLink}
/> :
<DeleteWorkspaceCTA/>
}
</>} </>}
</div> </div>
</div> </div>

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

@@ -177,7 +177,7 @@ describe('limits_reached_banner', () => {
const store = mockStore(state); const store = mockStore(state);
const spies = makeSpies(); const spies = makeSpies();
const mockOpenSalesLink = jest.fn(); const mockOpenSalesLink = jest.fn();
spies.useOpenSalesLink.mockReturnValue(mockOpenSalesLink); spies.useOpenSalesLink.mockReturnValue([mockOpenSalesLink, '']);
spies.useGetUsageDeltas.mockReturnValue(someLimitReached); spies.useGetUsageDeltas.mockReturnValue(someLimitReached);
renderWithIntl(<Provider store={store}><LimitReachedBanner product={free}/></Provider>); renderWithIntl(<Provider store={store}><LimitReachedBanner product={free}/></Provider>);
screen.getByText(titleFree); screen.getByText(titleFree);

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

@@ -5,8 +5,6 @@ import React from 'react';
import {useIntl, FormattedMessage} from 'react-intl'; import {useIntl, FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux'; import {useSelector} from 'react-redux';
import {SalesInquiryIssue} from 'selectors/cloud';
import {CloudProducts} from 'utils/constants'; import {CloudProducts} from 'utils/constants';
import {anyUsageDeltaExceededLimit} from 'utils/limits'; import {anyUsageDeltaExceededLimit} from 'utils/limits';
@@ -33,7 +31,7 @@ const LimitReachedBanner = (props: Props) => {
const hasDismissedBanner = useSelector(getHasDismissedSystemConsoleLimitReached); const hasDismissedBanner = useSelector(getHasDismissedSystemConsoleLimitReached);
const openSalesLink = useOpenSalesLink(props.product?.sku === CloudProducts.PROFESSIONAL ? SalesInquiryIssue.UpgradeEnterprise : undefined); const [openSalesLink] = useOpenSalesLink();
const openPricingModal = useOpenPricingModal(); const openPricingModal = useOpenPricingModal();
const saveBool = useSaveBool(); const saveBool = useSaveBool();
if (hasDismissedBanner || !someLimitExceeded || !props.product || (props.product.sku !== CloudProducts.STARTER)) { if (hasDismissedBanner || !someLimitExceeded || !props.product || (props.product.sku !== CloudProducts.STARTER)) {

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

@@ -11,8 +11,6 @@ import {
getSubscriptionProduct, getSubscriptionProduct,
} from 'mattermost-redux/selectors/entities/cloud'; } from 'mattermost-redux/selectors/entities/cloud';
import {SalesInquiryIssue} from 'selectors/cloud';
import {CloudProducts} from 'utils/constants'; import {CloudProducts} from 'utils/constants';
import {asGBString, fallbackStarterLimits, hasSomeLimits} from 'utils/limits'; import {asGBString, fallbackStarterLimits, hasSomeLimits} from 'utils/limits';
@@ -32,7 +30,7 @@ const Limits = (): JSX.Element | null => {
const subscriptionProduct = useSelector(getSubscriptionProduct); const subscriptionProduct = useSelector(getSubscriptionProduct);
const [cloudLimits, limitsLoaded] = useGetLimits(); const [cloudLimits, limitsLoaded] = useGetLimits();
const usage = useGetUsage(); const usage = useGetUsage();
const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); const [openSalesLink] = useOpenSalesLink();
const openPricingModal = useOpenPricingModal(); const openPricingModal = useOpenPricingModal();
if (!subscriptionProduct || !limitsLoaded || !hasSomeLimits(cloudLimits)) { if (!subscriptionProduct || !limitsLoaded || !hasSomeLimits(cloudLimits)) {

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

@@ -10,7 +10,6 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
import {SalesInquiryIssue} from 'selectors/cloud';
import {getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud'; import {getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import {savePreferences} from 'mattermost-redux/actions/preferences'; import {savePreferences} from 'mattermost-redux/actions/preferences';
@@ -79,7 +78,7 @@ const ToYearlyNudgeBannerDismissable = () => {
const ToYearlyNudgeBanner = () => { const ToYearlyNudgeBanner = () => {
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
const openSalesLink = useOpenSalesLink(SalesInquiryIssue.AboutPurchasing); const [openSalesLink] = useOpenSalesLink();
const openPurchaseModal = useOpenCloudPurchaseModal({}); const openPurchaseModal = useOpenCloudPurchaseModal({});
const product = useSelector(selectSubscriptionProduct); const product = useSelector(selectSubscriptionProduct);

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

@@ -7,6 +7,7 @@ import {useDispatch, useSelector} from 'react-redux';
import IconMessage from 'components/purchase_modal/icon_message'; import IconMessage from 'components/purchase_modal/icon_message';
import FullScreenModal from 'components/widgets/modals/full_screen_modal'; import FullScreenModal from 'components/widgets/modals/full_screen_modal';
import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
import {closeModal} from 'actions/views/modals'; import {closeModal} from 'actions/views/modals';
import {isModalOpen} from 'selectors/views/modals'; import {isModalOpen} from 'selectors/views/modals';
@@ -14,9 +15,6 @@ import {GlobalState} from 'types/store';
import './result_modal.scss'; import './result_modal.scss';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import {InquiryType} from 'selectors/cloud';
type Props = { type Props = {
onHide?: () => void; onHide?: () => void;
icon: JSX.Element; icon: JSX.Element;
@@ -33,7 +31,7 @@ type Props = {
export default function ResultModal(props: Props) { export default function ResultModal(props: Props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const openContactUs = useOpenSalesLink(undefined, InquiryType.Technical); const [openContactSupport] = useOpenCloudZendeskSupportForm('Delete workspace', '');
const isResultModalOpen = useSelector((state: GlobalState) => const isResultModalOpen = useSelector((state: GlobalState) =>
isModalOpen(state, props.identifier), isModalOpen(state, props.identifier),
@@ -64,14 +62,13 @@ export default function ResultModal(props: Props) {
buttonHandler={props.primaryButtonHandler} buttonHandler={props.primaryButtonHandler}
className={'success'} className={'success'}
formattedTertiaryButonText={ formattedTertiaryButonText={
props.contactSupportButtonVisible ? props.contactSupportButtonVisible ? (
<FormattedMessage <FormattedMessage
id={'admin.billing.deleteWorkspace.resultModal.ContactSupport'} id={'admin.billing.deleteWorkspace.resultModal.ContactSupport'}
defaultMessage={'Contact Support'} defaultMessage={'Contact Support'}
/> : />) : undefined
undefined
} }
tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactUs : undefined} tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactSupport : undefined}
/> />
</div> </div>
</FullScreenModal> </FullScreenModal>

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

@@ -17,7 +17,6 @@ describe('components/feature_discovery', () => {
<FeatureDiscovery <FeatureDiscovery
featureName='test' featureName='test'
minimumSKURequiredForFeature={LicenseSkus.Professional} minimumSKURequiredForFeature={LicenseSkus.Professional}
contactSalesLink='/sales'
titleID='translation.test.title' titleID='translation.test.title'
titleDefault='Foo' titleDefault='Foo'
copyID='translation.test.copy' copyID='translation.test.copy'
@@ -46,7 +45,6 @@ describe('components/feature_discovery', () => {
<FeatureDiscovery <FeatureDiscovery
featureName='test' featureName='test'
minimumSKURequiredForFeature={LicenseSkus.Professional} minimumSKURequiredForFeature={LicenseSkus.Professional}
contactSalesLink='/sales'
titleID='translation.test.title' titleID='translation.test.title'
titleDefault='Foo' titleDefault='Foo'
copyID='translation.test.copy' copyID='translation.test.copy'
@@ -76,7 +74,6 @@ describe('components/feature_discovery', () => {
<FeatureDiscovery <FeatureDiscovery
featureName='test' featureName='test'
minimumSKURequiredForFeature={LicenseSkus.Professional} minimumSKURequiredForFeature={LicenseSkus.Professional}
contactSalesLink='/sales'
titleID='translation.test.title' titleID='translation.test.title'
titleDefault='Foo' titleDefault='Foo'
copyID='translation.test.copy' copyID='translation.test.copy'

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

@@ -6,9 +6,6 @@ import {FormattedMessage} from 'react-intl';
import FormattedMarkdownMessage from 'components/formatted_markdown_message'; import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import {AnalyticsRow} from '@mattermost/types/admin';
import {ClientLicense} from '@mattermost/types/config';
import {EmbargoedEntityTrialError} from 'components/admin_console/license_settings/trial_banner/trial_banner'; import {EmbargoedEntityTrialError} from 'components/admin_console/license_settings/trial_banner/trial_banner';
import AlertBanner from 'components/alert_banner'; import AlertBanner from 'components/alert_banner';
import LoadingSpinner from 'components/widgets/loading/loading_spinner'; import LoadingSpinner from 'components/widgets/loading/loading_spinner';
@@ -17,17 +14,22 @@ import PurchaseLink from 'components/announcement_bar/purchase_link/purchase_lin
import ContactUsButton from 'components/announcement_bar/contact_sales/contact_us'; import ContactUsButton from 'components/announcement_bar/contact_sales/contact_us';
import PurchaseModal from 'components/purchase_modal'; import PurchaseModal from 'components/purchase_modal';
import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn'; import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn';
import ExternalLink from 'components/external_link';
import {ModalIdentifiers, TELEMETRY_CATEGORIES, AboutLinks, LicenseLinks, LicenseSkus} from 'utils/constants'; import {ModalIdentifiers, TELEMETRY_CATEGORIES, AboutLinks, LicenseLinks, LicenseSkus} from 'utils/constants';
import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils'; import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
import * as Utils from 'utils/utils'; import * as Utils from 'utils/utils';
import {goToMattermostContactSalesForm} from 'utils/contact_support_sales';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
import {ModalData} from 'types/actions'; import {ModalData} from 'types/actions';
import {ClientLicense} from '@mattermost/types/config';
import {AnalyticsRow} from '@mattermost/types/admin';
import {CloudCustomer} from '@mattermost/types/cloud';
import './feature_discovery.scss'; import './feature_discovery.scss';
import ExternalLink from 'components/external_link';
type Props = { type Props = {
featureName: string; featureName: string;
@@ -56,7 +58,7 @@ type Props = {
hadPrevCloudTrial: boolean; hadPrevCloudTrial: boolean;
isSubscriptionLoaded: boolean; isSubscriptionLoaded: boolean;
isPaidSubscription: boolean; isPaidSubscription: boolean;
contactSalesLink: string; customer?: CloudCustomer;
} }
type State = { type State = {
@@ -97,6 +99,16 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
}); });
} }
contactSalesFunc = () => {
const {customer, isCloud} = this.props;
const customerEmail = customer?.email || '';
const firstName = customer?.contact_first_name || '';
const lastName = customer?.contact_last_name || '';
const companyName = customer?.name || '';
const utmMedium = isCloud ? 'in-product-cloud' : 'in-product';
goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, 'mattermost', utmMedium);
}
renderPostTrialCta = () => { renderPostTrialCta = () => {
const { const {
minimumSKURequiredForFeature, minimumSKURequiredForFeature,
@@ -110,7 +122,7 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
data-testid='featureDiscovery_primaryCallToAction' data-testid='featureDiscovery_primaryCallToAction'
onClick={() => { onClick={() => {
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
window.open(LicenseLinks.CONTACT_SALES, '_blank'); this.contactSalesFunc();
}} }}
> >
<FormattedMessage <FormattedMessage
@@ -161,7 +173,6 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
hadPrevCloudTrial, hadPrevCloudTrial,
isPaidSubscription, isPaidSubscription,
minimumSKURequiredForFeature, minimumSKURequiredForFeature,
contactSalesLink,
} = this.props; } = this.props;
const canRequestCloudFreeTrial = isCloud && !isCloudTrial && !hadPrevCloudTrial && !isPaidSubscription; const canRequestCloudFreeTrial = isCloud && !isCloudTrial && !hadPrevCloudTrial && !isPaidSubscription;
@@ -217,11 +228,10 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
onClick={() => { onClick={() => {
if (isCloud) { if (isCloud) {
trackEvent(TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); trackEvent(TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
window.open(contactSalesLink, '_blank');
} else { } else {
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
window.open(LicenseLinks.CONTACT_SALES, '_blank');
} }
this.contactSalesFunc();
}} }}
> >
<FormattedMessage <FormattedMessage

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

@@ -7,11 +7,9 @@ import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
import {getPrevTrialLicense} from 'mattermost-redux/actions/admin'; import {getPrevTrialLicense} from 'mattermost-redux/actions/admin';
import {getCloudSubscription} from 'mattermost-redux/actions/cloud'; import {getCloudSubscription} from 'mattermost-redux/actions/cloud';
import {Action, GenericAction} from 'mattermost-redux/types/actions'; import {Action, GenericAction} from 'mattermost-redux/types/actions';
import {checkHadPriorTrial} from 'mattermost-redux/selectors/entities/cloud'; import {checkHadPriorTrial, getCloudCustomer} from 'mattermost-redux/selectors/entities/cloud';
import {getLicense} from 'mattermost-redux/selectors/entities/general'; import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {getCloudContactUsLink, InquiryType} from 'selectors/cloud';
import {ModalData} from 'types/actions'; import {ModalData} from 'types/actions';
import {GlobalState} from 'types/store'; import {GlobalState} from 'types/store';
@@ -30,7 +28,7 @@ function mapStateToProps(state: GlobalState) {
const isCloud = isCloudLicense(license); const isCloud = isCloudLicense(license);
const hasPriorTrial = checkHadPriorTrial(state); const hasPriorTrial = checkHadPriorTrial(state);
const isCloudTrial = subscription?.is_free_trial === 'true'; const isCloudTrial = subscription?.is_free_trial === 'true';
const contactSalesLink = getCloudContactUsLink(state)(InquiryType.Sales); const customer = getCloudCustomer(state);
return { return {
stats: state.entities.admin.analytics, stats: state.entities.admin.analytics,
prevTrialLicense: state.entities.admin.prevTrialLicense, prevTrialLicense: state.entities.admin.prevTrialLicense,
@@ -39,7 +37,7 @@ function mapStateToProps(state: GlobalState) {
isSubscriptionLoaded: subscription !== undefined && subscription !== null, isSubscriptionLoaded: subscription !== undefined && subscription !== null,
hadPrevCloudTrial: hasPriorTrial, hadPrevCloudTrial: hasPriorTrial,
isPaidSubscription: isCloud && license?.SkuShortName !== LicenseSkus.Starter && !isCloudTrial, isPaidSubscription: isCloud && license?.SkuShortName !== LicenseSkus.Starter && !isCloudTrial,
contactSalesLink, customer,
}; };
} }

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

@@ -2,13 +2,46 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import {Provider} from 'react-redux';
import {LicenseSkus} from 'utils/constants'; import {LicenseSkus} from 'utils/constants';
import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import EnterpriseEditionRightPanel, {EnterpriseEditionProps} from './enterprise_edition_right_panel'; import EnterpriseEditionRightPanel, {EnterpriseEditionProps} from './enterprise_edition_right_panel';
const initialState = {
views: {
announcementBar: {
announcementBarState: {
announcementBarCount: 1,
},
},
},
entities: {
general: {
config: {
CWSURL: '',
},
license: {
IsLicensed: 'true',
Cloud: 'true',
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {roles: 'system_user'},
},
},
preferences: {
myPreferences: {},
},
cloud: {},
},
};
describe('components/admin_console/license_settings/enterprise_edition/enterprise_edition_right_panel', () => { describe('components/admin_console/license_settings/enterprise_edition/enterprise_edition_right_panel', () => {
const license = { const license = {
IsLicensed: 'true', IsLicensed: 'true',
@@ -28,8 +61,11 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
} as EnterpriseEditionProps; } as EnterpriseEditionProps;
test('should render for no Gov no Trial no Enterprise', () => { test('should render for no Gov no Trial no Enterprise', () => {
const store = mockStore(initialState);
const wrapper = mountWithIntl( const wrapper = mountWithIntl(
<EnterpriseEditionRightPanel {...props}/>, <Provider store={store}>
<EnterpriseEditionRightPanel {...props}/>
</Provider>,
); );
expect(wrapper.find('.upgrade-title').text()).toEqual('Upgrade to the Enterprise Plan'); expect(wrapper.find('.upgrade-title').text()).toEqual('Upgrade to the Enterprise Plan');
@@ -43,11 +79,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
}); });
test('should render for Gov no Trial no Enterprise', () => { test('should render for Gov no Trial no Enterprise', () => {
const store = mockStore(initialState);
const wrapper = mountWithIntl( const wrapper = mountWithIntl(
<EnterpriseEditionRightPanel <Provider store={store}>
license={{...props.license, IsGovSku: 'true'}} <EnterpriseEditionRightPanel
isTrialLicense={props.isTrialLicense} license={{...props.license, IsGovSku: 'true'}}
/>, isTrialLicense={props.isTrialLicense}
/>
</Provider>,
); );
expect(wrapper.find('.upgrade-title').text()).toEqual('Upgrade to the Enterprise Gov Plan'); expect(wrapper.find('.upgrade-title').text()).toEqual('Upgrade to the Enterprise Gov Plan');
@@ -61,11 +100,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
}); });
test('should render for Enterprise no Trial', () => { test('should render for Enterprise no Trial', () => {
const store = mockStore(initialState);
const wrapper = mountWithIntl( const wrapper = mountWithIntl(
<EnterpriseEditionRightPanel <Provider store={store}>
license={{...props.license, SkuShortName: LicenseSkus.Enterprise}} <EnterpriseEditionRightPanel
isTrialLicense={props.isTrialLicense} license={{...props.license, SkuShortName: LicenseSkus.Enterprise}}
/>, isTrialLicense={props.isTrialLicense}
/>
</Provider>,
); );
expect(wrapper.find('.upgrade-title').text()).toEqual('Need to increase your headcount?'); expect(wrapper.find('.upgrade-title').text()).toEqual('Need to increase your headcount?');
@@ -73,11 +115,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
}); });
test('should render for E20 no Trial', () => { test('should render for E20 no Trial', () => {
const store = mockStore(initialState);
const wrapper = mountWithIntl( const wrapper = mountWithIntl(
<EnterpriseEditionRightPanel <Provider store={store}>
license={{...props.license, SkuShortName: LicenseSkus.E20}} <EnterpriseEditionRightPanel
isTrialLicense={props.isTrialLicense} license={{...props.license, SkuShortName: LicenseSkus.E20}}
/>, isTrialLicense={props.isTrialLicense}
/>
</Provider>,
); );
expect(wrapper.find('.upgrade-title').text()).toEqual('Need to increase your headcount?'); expect(wrapper.find('.upgrade-title').text()).toEqual('Need to increase your headcount?');
@@ -85,11 +130,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
}); });
test('should render for Trial no Gov', () => { test('should render for Trial no Gov', () => {
const store = mockStore(initialState);
const wrapper = mountWithIntl( const wrapper = mountWithIntl(
<EnterpriseEditionRightPanel <Provider store={store}>
license={props.license} <EnterpriseEditionRightPanel
isTrialLicense={true} license={props.license}
/>, isTrialLicense={true}
/>
</Provider>,
); );
expect(wrapper.find('.upgrade-title').text()).toEqual('Purchase the Enterprise Plan'); expect(wrapper.find('.upgrade-title').text()).toEqual('Purchase the Enterprise Plan');
@@ -97,11 +145,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
}); });
test('should render for Trial Gov', () => { test('should render for Trial Gov', () => {
const store = mockStore(initialState);
const wrapper = mountWithIntl( const wrapper = mountWithIntl(
<EnterpriseEditionRightPanel <Provider store={store}>
license={{...props.license, IsGovSku: 'true'}} <EnterpriseEditionRightPanel
isTrialLicense={true} license={{...props.license, IsGovSku: 'true'}}
/>, isTrialLicense={true}
/>
</Provider>,
); );
expect(wrapper.find('.upgrade-title').text()).toEqual('Purchase the Enterprise Gov Plan'); expect(wrapper.find('.upgrade-title').text()).toEqual('Purchase the Enterprise Gov Plan');

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

@@ -12,6 +12,37 @@ import mockStore from 'tests/test_store';
import RenewalLicenseCard from './renew_license_card'; import RenewalLicenseCard from './renew_license_card';
const initialState = {
views: {
announcementBar: {
announcementBarState: {
announcementBarCount: 1,
},
},
},
entities: {
general: {
config: {
CWSURL: '',
},
license: {
IsLicensed: 'true',
Cloud: 'true',
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {roles: 'system_user'},
},
},
preferences: {
myPreferences: {},
},
cloud: {},
},
};
const actImmediate = (wrapper: ReactWrapper) => const actImmediate = (wrapper: ReactWrapper) =>
act( act(
() => () =>
@@ -47,7 +78,7 @@ describe('components/RenewalLicenseCard', () => {
}); });
}); });
getRenewalLinkSpy.mockImplementation(() => promise); getRenewalLinkSpy.mockImplementation(() => promise);
const store = mockStore({}); const store = mockStore(initialState);
const wrapper = mountWithIntl(<Provider store={store}><RenewalLicenseCard {...props}/></Provider>); const wrapper = mountWithIntl(<Provider store={store}><RenewalLicenseCard {...props}/></Provider>);
// wait for the promise to resolve and component to update // wait for the promise to resolve and component to update
@@ -64,7 +95,7 @@ describe('components/RenewalLicenseCard', () => {
reject(new Error('License cannot be renewed from portal')); reject(new Error('License cannot be renewed from portal'));
}); });
getRenewalLinkSpy.mockImplementation(() => promise); getRenewalLinkSpy.mockImplementation(() => promise);
const store = mockStore({}); const store = mockStore(initialState);
const wrapper = mountWithIntl(<Provider store={store}><RenewalLicenseCard {...props}/></Provider>); const wrapper = mountWithIntl(<Provider store={store}><RenewalLicenseCard {...props}/></Provider>);
// wait for the promise to resolve and component to update // wait for the promise to resolve and component to update

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

@@ -18,8 +18,9 @@ import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {GlobalState} from '@mattermost/types/store'; import {GlobalState} from '@mattermost/types/store';
import {CloudLinks, ConsolePages, DocLinks, LicenseLinks} from 'utils/constants'; import {CloudLinks, ConsolePages, DocLinks} from 'utils/constants';
import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils'; import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
export type DataModel = { export type DataModel = {
[key: string]: { [key: string]: {
@@ -84,8 +85,10 @@ const useMetricsData = () => {
const isEnterpriseLicense = isEnterpriseOrE20License(license); const isEnterpriseLicense = isEnterpriseOrE20License(license);
const isStarterLicense = getIsStarterLicense(license); const isStarterLicense = getIsStarterLicense(license);
const [, contactSalesLink] = useOpenSalesLink();
const trialOrEnterpriseCtaConfig = { const trialOrEnterpriseCtaConfig = {
configUrl: canStartTrial ? ConsolePages.LICENSE : LicenseLinks.CONTACT_SALES, configUrl: canStartTrial ? ConsolePages.LICENSE : contactSalesLink,
configText: canStartTrial ? formatMessage({id: 'admin.reporting.workspace_optimization.cta.startTrial', defaultMessage: 'Start trial'}) : formatMessage({id: 'admin.reporting.workspace_optimization.cta.upgradeLicense', defaultMessage: 'Contact sales'}), configText: canStartTrial ? formatMessage({id: 'admin.reporting.workspace_optimization.cta.startTrial', defaultMessage: 'Start trial'}) : formatMessage({id: 'admin.reporting.workspace_optimization.cta.upgradeLicense', defaultMessage: 'Contact sales'}),
}; };

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

@@ -6,8 +6,9 @@ import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import './contact_us.scss'; import './contact_us.scss';
import {LicenseLinks} from '../../../utils/constants';
export interface Props { export interface Props {
buttonTextElement?: JSX.Element; buttonTextElement?: JSX.Element;
@@ -16,10 +17,12 @@ export interface Props {
} }
const ContactUsButton: React.FC<Props> = (props: Props) => { const ContactUsButton: React.FC<Props> = (props: Props) => {
const [openContactSales] = useOpenSalesLink();
const handleContactUsLinkClick = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { const handleContactUsLinkClick = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
trackEvent('admin', props.eventID || 'in_trial_contact_sales'); trackEvent('admin', props.eventID || 'in_trial_contact_sales');
window.open(LicenseLinks.CONTACT_SALES, '_blank'); openContactSales();
}; };
return ( return (

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

@@ -15,7 +15,8 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {PreferenceType} from '@mattermost/types/preferences'; import {PreferenceType} from '@mattermost/types/preferences';
import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck'; import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck';
import {LicenseLinks, StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import {StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants';
import './overage_users_banner.scss'; import './overage_users_banner.scss';
@@ -34,6 +35,7 @@ const adminHasDismissed = ({preferenceName, overagePreferences, isWarningBanner}
}; };
const OverageUsersBanner = () => { const OverageUsersBanner = () => {
const [openContactSales] = useOpenSalesLink();
const dispatch = useDispatch(); const dispatch = useDispatch();
const stats = useSelector((state: GlobalState) => state.entities.admin.analytics) || {}; const stats = useSelector((state: GlobalState) => state.entities.admin.analytics) || {};
const isAdmin = useSelector(isCurrentUserSystemAdmin); const isAdmin = useSelector(isCurrentUserSystemAdmin);
@@ -90,7 +92,7 @@ const OverageUsersBanner = () => {
const handleContactSalesClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { const handleContactSalesClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
trackEventFn('Contact Sales'); trackEventFn('Contact Sales');
window.open(LicenseLinks.CONTACT_SALES, '_blank'); openContactSales();
}; };
const handleClick = isExpandable ? handleUpdateSeatsSelfServeClick : handleContactSalesClick; const handleClick = isExpandable ? handleUpdateSeatsSelfServeClick : handleContactSalesClick;

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

@@ -7,7 +7,7 @@ import {fireEvent, screen} from '@testing-library/react';
import {DeepPartial} from '@mattermost/types/utilities'; import {DeepPartial} from '@mattermost/types/utilities';
import {GlobalState} from 'types/store'; import {GlobalState} from 'types/store';
import {General} from 'mattermost-redux/constants'; import {General} from 'mattermost-redux/constants';
import {LicenseLinks, OverActiveUserLimits, Preferences, StatTypes} from 'utils/constants'; import {OverActiveUserLimits, Preferences, StatTypes} from 'utils/constants';
import {renderWithIntlAndStore} from 'tests/react_testing_utils'; import {renderWithIntlAndStore} from 'tests/react_testing_utils';
import {savePreferences} from 'mattermost-redux/actions/preferences'; import {savePreferences} from 'mattermost-redux/actions/preferences';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
@@ -249,7 +249,10 @@ describe('components/overage_users_banner', () => {
fireEvent.click(screen.getByText(contactSalesTextLink)); fireEvent.click(screen.getByText(contactSalesTextLink));
expect(windowSpy).toBeCalledTimes(1); expect(windowSpy).toBeCalledTimes(1);
expect(windowSpy).toBeCalledWith(LicenseLinks.CONTACT_SALES, '_blank');
// only the email is encoded and other params are empty. See logic for useOpenSalesLink hook
const salesLinkWithEncodedParams = 'https://mattermost.com/contact-sales/?qk=&qp=&qw=&qx=dGVzdEBtYXR0ZXJtb3N0LmNvbQ==&utm_source=mattermost&utm_medium=in-product';
expect(windowSpy).toBeCalledWith(salesLinkWithEncodedParams, '_blank');
expect(trackEvent).toBeCalledTimes(1); expect(trackEvent).toBeCalledTimes(1);
expect(trackEvent).toBeCalledWith('insights', 'click_true_up_warning', { expect(trackEvent).toBeCalledWith('insights', 'click_true_up_warning', {
cta: 'Contact Sales', cta: 'Contact Sales',
@@ -368,7 +371,10 @@ describe('components/overage_users_banner', () => {
fireEvent.click(screen.getByText(contactSalesTextLink)); fireEvent.click(screen.getByText(contactSalesTextLink));
expect(windowSpy).toBeCalledTimes(1); expect(windowSpy).toBeCalledTimes(1);
expect(windowSpy).toBeCalledWith(LicenseLinks.CONTACT_SALES, '_blank');
// only the email is encoded and other params are empty. See logic for useOpenSalesLink hook
const salesLinkWithEncodedParams = 'https://mattermost.com/contact-sales/?qk=&qp=&qw=&qx=dGVzdEBtYXR0ZXJtb3N0LmNvbQ==&utm_source=mattermost&utm_medium=in-product';
expect(windowSpy).toBeCalledWith(salesLinkWithEncodedParams, '_blank');
expect(trackEvent).toBeCalledTimes(1); expect(trackEvent).toBeCalledTimes(1);
expect(trackEvent).toBeCalledWith('insights', 'click_true_up_error', { expect(trackEvent).toBeCalledWith('insights', 'click_true_up_error', {
cta: 'Contact Sales', cta: 'Contact Sales',

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

@@ -3,13 +3,46 @@
import React from 'react'; import React from 'react';
import {ReactWrapper} from 'enzyme'; import {ReactWrapper} from 'enzyme';
import {Provider} from 'react-redux';
import {act} from 'react-dom/test-utils'; import {act} from 'react-dom/test-utils';
import {Client4} from 'mattermost-redux/client'; import {Client4} from 'mattermost-redux/client';
import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import RenewalLink from './renewal_link'; import RenewalLink from './renewal_link';
const initialState = {
views: {
announcementBar: {
announcementBarState: {
announcementBarCount: 1,
},
},
},
entities: {
general: {
config: {
CWSURL: '',
},
license: {
IsLicensed: 'true',
Cloud: 'true',
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {roles: 'system_user'},
},
},
preferences: {
myPreferences: {},
},
cloud: {},
},
};
const actImmediate = (wrapper: ReactWrapper) => const actImmediate = (wrapper: ReactWrapper) =>
act( act(
() => () =>
@@ -40,7 +73,8 @@ describe('components/RenewalLink', () => {
}); });
}); });
getRenewalLinkSpy.mockImplementation(() => promise); getRenewalLinkSpy.mockImplementation(() => promise);
const wrapper = mountWithIntl(<RenewalLink {...props}/>); const store = mockStore(initialState);
const wrapper = mountWithIntl(<Provider store={store}><RenewalLink {...props}/></Provider>);
// wait for the promise to resolve and component to update // wait for the promise to resolve and component to update
await actImmediate(wrapper); await actImmediate(wrapper);
@@ -54,7 +88,8 @@ describe('components/RenewalLink', () => {
reject(new Error('License cannot be renewed from portal')); reject(new Error('License cannot be renewed from portal'));
}); });
getRenewalLinkSpy.mockImplementation(() => promise); getRenewalLinkSpy.mockImplementation(() => promise);
const wrapper = mountWithIntl(<RenewalLink {...props}/>); const store = mockStore(initialState);
const wrapper = mountWithIntl(<Provider store={store}><RenewalLink {...props}/></Provider>);
// wait for the promise to resolve and component to update // wait for the promise to resolve and component to update
await actImmediate(wrapper); await actImmediate(wrapper);

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

@@ -11,9 +11,9 @@ import {trackEvent} from 'actions/telemetry_actions';
import {ModalData} from 'types/actions'; import {ModalData} from 'types/actions';
import { import {
LicenseLinks,
ModalIdentifiers, ModalIdentifiers,
} from 'utils/constants'; } from 'utils/constants';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import NoInternetConnection from '../no_internet_connection/no_internet_connection'; import NoInternetConnection from '../no_internet_connection/no_internet_connection';
@@ -31,6 +31,9 @@ export interface RenewalLinkProps {
const RenewalLink = (props: RenewalLinkProps) => { const RenewalLink = (props: RenewalLinkProps) => {
const [renewalLink, setRenewalLink] = useState(''); const [renewalLink, setRenewalLink] = useState('');
const [manualInterventionRequired, setManualInterventionRequired] = useState(false); const [manualInterventionRequired, setManualInterventionRequired] = useState(false);
const [openContactSales] = useOpenSalesLink();
useEffect(() => { useEffect(() => {
Client4.getRenewalLink().then(({renewal_link: renewalLinkParam}) => { Client4.getRenewalLink().then(({renewal_link: renewalLinkParam}) => {
try { try {
@@ -55,7 +58,7 @@ const RenewalLink = (props: RenewalLinkProps) => {
} }
window.open(renewalLink, '_blank'); window.open(renewalLink, '_blank');
} else if (manualInterventionRequired) { } else if (manualInterventionRequired) {
window.open(LicenseLinks.CONTACT_SALES, '_blank'); openContactSales();
} else { } else {
showConnectionErrorModal(); showConnectionErrorModal();
} }

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

@@ -0,0 +1,37 @@
.shipping-address-section {
display: flex;
align-content: flex-start;
padding-bottom: 24px;
font-weight: normal;
button.no-style {
padding-left: 0;
border: none;
background: transparent;
outline: unset;
text-align: left;
&:focus {
outline: unset;
}
}
#address-same-than-billing-address {
width: 17px;
height: 17px;
flex-shrink: 0;
}
.Form-checkbox-label {
padding-left: 12px;
cursor: default;
font-family: 'Open Sans', sans-serif;
vertical-align: middle;
}
.billing_address_btn_text {
color: var(--center-channel-color);
font-family: 'Open Sans', sans-serif;
font-weight: bold;
}
}

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

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import './choose_different_shipping.scss';
interface Props {
shippingIsSame: boolean;
setShippingIsSame: (different: boolean) => void;
}
export default function ChooseDifferentShipping(props: Props) {
const intl = useIntl();
const toggle = () => props.setShippingIsSame(!props.shippingIsSame);
return (
<div className='shipping-address-section'>
<input
id='address-same-than-billing-address'
className='Form-checkbox-input'
name='terms'
type='checkbox'
checked={props.shippingIsSame}
onChange={toggle}
/>
<span className='Form-checkbox-label'>
<button
onClick={toggle}
type='button'
className='no-style'
>
<span className='billing_address_btn_text'>
{intl.formatMessage({
id: 'admin.billing.subscription.complianceScreenShippingSameAsBilling',
defaultMessage:
'My shipping address is the same as my billing address',
})}
</span>
</button>
</span>
</div>
);
}

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

@@ -13,9 +13,8 @@ import PaymentFailedSvg from 'components/common/svg_images_components/payment_fa
import IconMessage from 'components/purchase_modal/icon_message'; import IconMessage from 'components/purchase_modal/icon_message';
import FullScreenModal from 'components/widgets/modals/full_screen_modal'; import FullScreenModal from 'components/widgets/modals/full_screen_modal';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
import {InquiryType} from 'selectors/cloud';
import {closeModal} from 'actions/views/modals'; import {closeModal} from 'actions/views/modals';
import {ModalIdentifiers} from 'utils/constants'; import {ModalIdentifiers} from 'utils/constants';
import {isModalOpen} from 'selectors/views/modals'; import {isModalOpen} from 'selectors/views/modals';
@@ -31,7 +30,8 @@ type Props = {
function ErrorModal(props: Props) { function ErrorModal(props: Props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const subscriptionProduct = useSelector(getSubscriptionProduct); const subscriptionProduct = useSelector(getSubscriptionProduct);
const openContactUs = useOpenSalesLink(undefined, InquiryType.Technical);
const [openContactSupport] = useOpenCloudZendeskSupportForm('Cloud Subscription', '');
const isSuccessModalOpen = useSelector((state: GlobalState) => const isSuccessModalOpen = useSelector((state: GlobalState) =>
isModalOpen(state, ModalIdentifiers.ERROR_MODAL), isModalOpen(state, ModalIdentifiers.ERROR_MODAL),
@@ -97,7 +97,7 @@ function ErrorModal(props: Props) {
} }
/> />
} }
tertiaryButtonHandler={openContactUs} tertiaryButtonHandler={openContactSupport}
buttonHandler={onBackButtonPress} buttonHandler={onBackButtonPress}
className={'success'} className={'success'}
/> />

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

@@ -3,11 +3,35 @@
import {useSelector} from 'react-redux'; import {useSelector} from 'react-redux';
import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud'; import {getCloudCustomer, isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {buildMMURL, goToMattermostContactSalesForm} from 'utils/contact_support_sales';
import {LicenseLinks} from 'utils/constants';
export default function useOpenSalesLink(issue?: SalesInquiryIssue, inquireType: InquiryType = InquiryType.Sales) { export default function useOpenSalesLink(): [() => void, string] {
const contactSalesLink = useSelector(getCloudContactUsLink)(inquireType, issue); const isCloud = useSelector(isCurrentLicenseCloud);
const customer = useSelector(getCloudCustomer);
const currentUser = useSelector(getCurrentUser);
let customerEmail = '';
let firstName = '';
let lastName = '';
let companyName = '';
const utmSource = 'mattermost';
let utmMedium = 'in-product';
return () => window.open(contactSalesLink, '_blank'); if (isCloud && customer) {
customerEmail = customer.email || '';
firstName = customer.contact_first_name || '';
lastName = customer.contact_last_name || '';
companyName = customer.name || '';
utmMedium = 'in-product-cloud';
} else {
customerEmail = currentUser.email || '';
}
const contactSalesLink = buildMMURL(LicenseLinks.CONTACT_SALES, firstName, lastName, companyName, customerEmail, utmSource, utmMedium);
const goToSalesLinkFunc = () => {
goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, utmSource, utmMedium);
};
return [goToSalesLinkFunc, contactSalesLink];
} }

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

@@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useSelector} from 'react-redux';
import {getCloudCustomer} from 'mattermost-redux/selectors/entities/cloud';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getCloudSupportLink, getSelfHostedSupportLink, goToCloudSupportForm, goToSelfHostedSupportForm} from 'utils/contact_support_sales';
export function useOpenCloudZendeskSupportForm(subject: string, description: string): [() => void, string] {
const customer = useSelector(getCloudCustomer);
const customerEmail = customer?.email || '';
const url = getCloudSupportLink(customerEmail, subject, description, window.location.host);
return [() => goToCloudSupportForm(customerEmail, subject, description, window.location.host), url];
}
export function useOpenSelfHostedZendeskSupportForm(subject: string): [() => void, string] {
const currentUser = useSelector(getCurrentUser);
const customerEmail = currentUser.email || '';
const url = getSelfHostedSupportLink(customerEmail, subject);
return [() => goToSelfHostedSupportForm(customerEmail, subject), url];
}

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

@@ -1,5 +1,7 @@
$dropdown_input_index: 999999;
.DropdownInput { .DropdownInput {
z-index: 999999; z-index: $dropdown_input_index;
&.Input_container { &.Input_container {
margin-top: 20px; margin-top: 20px;
@@ -37,7 +39,7 @@
} }
.DropdownInput__option > div { .DropdownInput__option > div {
z-index: 999999; z-index: $dropdown_input_index;
padding: 10px 24px; padding: 10px 24px;
cursor: pointer; cursor: pointer;
line-height: 16px; line-height: 16px;
@@ -51,3 +53,33 @@
.DropdownInput__option.focused > div { .DropdownInput__option.focused > div {
background-color: rgba(var(--center-channel-color-rgb), 0.08); background-color: rgba(var(--center-channel-color-rgb), 0.08);
} }
.second-dropdown-sibling-wrapper {
.DropdownInput {
z-index: $dropdown_input_index - 1;
}
.DropdownInput__option > div {
z-index: $dropdown_input_index - 1;
}
}
.third-dropdown-sibling-wrapper {
.DropdownInput {
z-index: $dropdown_input_index - 2;
}
.DropdownInput__option > div {
z-index: $dropdown_input_index - 2;
}
}
.fourth-dropdown-sibling-wrapper {
.DropdownInput {
z-index: $dropdown_input_index - 3;
}
.DropdownInput__option > div {
z-index: $dropdown_input_index - 3;
}
}

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

@@ -61,25 +61,27 @@ const AddressForm = (props: AddressFormProps) => {
{...props.title} {...props.title}
/> />
</div> </div>
<DropdownInput <div className='third-dropdown-sibling-wrapper'>
onChange={handleCountryChange} <DropdownInput
value={ onChange={handleCountryChange}
props.address.country ? {value: props.address.country, label: props.address.country} : undefined value={
} props.address.country ? {value: props.address.country, label: props.address.country} : undefined
options={COUNTRIES.map((country) => ({ }
value: country.name, options={COUNTRIES.map((country) => ({
label: country.name, value: country.name,
}))} label: country.name,
legend={formatMessage({ }))}
id: 'payment_form.country', legend={formatMessage({
defaultMessage: 'Country', id: 'payment_form.country',
})} defaultMessage: 'Country',
placeholder={formatMessage({ })}
id: 'payment_form.country', placeholder={formatMessage({
defaultMessage: 'Country', id: 'payment_form.country',
})} defaultMessage: 'Country',
name={'billing_dropdown'} })}
/> name={'billing_dropdown'}
/>
</div>
<div className='form-row'> <div className='form-row'>
<Input <Input
name='address' name='address'
@@ -122,7 +124,7 @@ const AddressForm = (props: AddressFormProps) => {
/> />
</div> </div>
<div className='form-row'> <div className='form-row'>
<div className='form-row-third-1 selector'> <div className='form-row-third-1 selector fourth-dropdown-sibling-wrapper'>
<StateSelector <StateSelector
country={props.address.country} country={props.address.country}
state={props.address.state} state={props.address.state}

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

@@ -12,7 +12,6 @@
.form-row-third-1 { .form-row-third-1 {
.DropdownInput { .DropdownInput {
z-index: 99999;
margin-top: 0; margin-top: 0;
} }
@@ -37,7 +36,6 @@
.DropdownInput { .DropdownInput {
position: relative; position: relative;
z-index: 999999;
height: 36px; height: 36px;
margin-bottom: 24px; margin-bottom: 24px;
font-weight: normal; font-weight: normal;

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

@@ -251,7 +251,7 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
/> />
</div> </div>
<div className='form-row'> <div className='form-row'>
<div className='form-row-third-1 selector'> <div className='form-row-third-1 selector second-dropdown-sibling-wrapper'>
<StateSelector <StateSelector
country={this.state.country} country={this.state.country}
state={this.state.state} state={this.state.state}

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

@@ -155,7 +155,7 @@ function PostPriorityPicker({
} }
} }
const feedbackLink = postAcknowledgementsEnabled ? 'https://forms.gle/noA8Azg7RdaBZtMB6' : 'https://forms.gle/XRb63s3KZqpLNyqr9'; const feedbackLink = postAcknowledgementsEnabled ? 'https://forms.gle/noA8Azg7RdaBZtMB6' : 'https://forms.gle/mMcRFQzyKAo9Sv49A';
return ( return (
<Picker <Picker

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

@@ -11,8 +11,7 @@ import {trackEvent} from 'actions/telemetry_actions';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import {LicenseLinks, TELEMETRY_CATEGORIES} from 'utils/constants'; import {TELEMETRY_CATEGORIES} from 'utils/constants';
import {SalesInquiryIssue} from 'selectors/cloud';
const StyledA = styled.a` const StyledA = styled.a`
color: var(--denim-button-bg); color: var(--denim-button-bg);
@@ -27,11 +26,7 @@ text-align: center;
function ContactSalesCTA() { function ContactSalesCTA() {
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); const [openSalesLink] = useOpenSalesLink();
const openSelfHostedLink = () => {
window.open(LicenseLinks.CONTACT_SALES, '_blank');
};
const isCloud = useSelector(isCurrentLicenseCloud); const isCloud = useSelector(isCurrentLicenseCloud);
@@ -42,11 +37,10 @@ function ContactSalesCTA() {
e.preventDefault(); e.preventDefault();
if (isCloud) { if (isCloud) {
trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales'); trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales');
openSalesLink();
} else { } else {
trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales'); trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales');
openSelfHostedLink();
} }
openSalesLink();
}} }}
> >
{formatMessage({id: 'pricing_modal.btn.contactSalesForQuote', defaultMessage: 'Contact Sales'})} {formatMessage({id: 'pricing_modal.btn.contactSalesForQuote', defaultMessage: 'Contact Sales'})}

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

@@ -10,8 +10,6 @@ import {CloudLinks, CloudProducts, LicenseSkus, ModalIdentifiers, MattermostFeat
import {fallbackStarterLimits, asGBString, hasSomeLimits} from 'utils/limits'; import {fallbackStarterLimits, asGBString, hasSomeLimits} from 'utils/limits';
import {findOnlyYearlyProducts, findProductBySku} from 'utils/products'; import {findOnlyYearlyProducts, findProductBySku} from 'utils/products';
import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
import {closeModal, openModal} from 'actions/views/modals'; import {closeModal, openModal} from 'actions/views/modals';
import {subscribeCloudSubscription} from 'actions/cloud'; import {subscribeCloudSubscription} from 'actions/cloud';
@@ -38,6 +36,8 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal'; import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
import useOpenDowngradeModal from 'components/common/hooks/useOpenDowngradeModal'; import useOpenDowngradeModal from 'components/common/hooks/useOpenDowngradeModal';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
import DowngradeTeamRemovalModal from './downgrade_team_removal_modal'; import DowngradeTeamRemovalModal from './downgrade_team_removal_modal';
@@ -64,15 +64,12 @@ function Content(props: ContentProps) {
const openPricingModalBackAction = useOpenPricingModal(); const openPricingModalBackAction = useOpenPricingModal();
const isAdmin = useSelector(isCurrentUserSystemAdmin); const isAdmin = useSelector(isCurrentUserSystemAdmin);
const contactSalesLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.UpgradeEnterprise);
const subscription = useSelector(selectCloudSubscription); const subscription = useSelector(selectCloudSubscription);
const currentProduct = useSelector(selectSubscriptionProduct); const currentProduct = useSelector(selectSubscriptionProduct);
const products = useSelector(selectCloudProducts); const products = useSelector(selectCloudProducts);
const yearlyProducts = findOnlyYearlyProducts(products || {}); // pricing modal should now only show yearly products const yearlyProducts = findOnlyYearlyProducts(products || {}); // pricing modal should now only show yearly products
const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical);
const currentSubscriptionIsMonthly = currentProduct?.recurring_interval === RecurringIntervals.MONTH; const currentSubscriptionIsMonthly = currentProduct?.recurring_interval === RecurringIntervals.MONTH;
const isEnterprise = currentProduct?.sku === CloudProducts.ENTERPRISE; const isEnterprise = currentProduct?.sku === CloudProducts.ENTERPRISE;
const isEnterpriseTrial = subscription?.is_free_trial === 'true'; const isEnterpriseTrial = subscription?.is_free_trial === 'true';
@@ -124,6 +121,8 @@ function Content(props: ContentProps) {
const freeTierText = (!isStarter && !currentSubscriptionIsMonthly) ? formatMessage({id: 'pricing_modal.btn.contactSupport', defaultMessage: 'Contact Support'}) : formatMessage({id: 'pricing_modal.btn.downgrade', defaultMessage: 'Downgrade'}); const freeTierText = (!isStarter && !currentSubscriptionIsMonthly) ? formatMessage({id: 'pricing_modal.btn.contactSupport', defaultMessage: 'Contact Support'}) : formatMessage({id: 'pricing_modal.btn.downgrade', defaultMessage: 'Downgrade'});
const adminProfessionalTierText = currentSubscriptionIsMonthlyProfessional ? formatMessage({id: 'pricing_modal.btn.switch_to_annual', defaultMessage: 'Switch to annual billing'}) : formatMessage({id: 'pricing_modal.btn.upgrade', defaultMessage: 'Upgrade'}); const adminProfessionalTierText = currentSubscriptionIsMonthlyProfessional ? formatMessage({id: 'pricing_modal.btn.switch_to_annual', defaultMessage: 'Switch to annual billing'}) : formatMessage({id: 'pricing_modal.btn.upgrade', defaultMessage: 'Upgrade'});
const [openContactSales] = useOpenSalesLink();
const [openContactSupport] = useOpenCloudZendeskSupportForm('Workspace downgrade', '');
const openCloudPurchaseModal = useOpenCloudPurchaseModal({}); const openCloudPurchaseModal = useOpenCloudPurchaseModal({});
const openCloudDelinquencyModal = useOpenCloudPurchaseModal({ const openCloudDelinquencyModal = useOpenCloudPurchaseModal({
isDelinquencyModal: true, isDelinquencyModal: true,
@@ -239,7 +238,7 @@ function Content(props: ContentProps) {
return { return {
action: () => { action: () => {
trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales'); trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales');
window.open(contactSalesLink, '_blank'); openContactSales();
}, },
text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}), text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}),
customClass: ButtonCustomiserClasses.active, customClass: ButtonCustomiserClasses.active,
@@ -350,7 +349,7 @@ function Content(props: ContentProps) {
buttonDetails={{ buttonDetails={{
action: () => { action: () => {
if (!isStarter && !currentSubscriptionIsMonthly) { if (!isStarter && !currentSubscriptionIsMonthly) {
window.open(contactSupportLink, '_blank'); openContactSupport();
return; return;
} }

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

@@ -6,7 +6,7 @@ import {Modal} from 'react-bootstrap';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import {CloudLinks, LicenseLinks, ModalIdentifiers, SelfHostedProducts, LicenseSkus, TELEMETRY_CATEGORIES, RecurringIntervals} from 'utils/constants'; import {CloudLinks, ModalIdentifiers, SelfHostedProducts, LicenseSkus, TELEMETRY_CATEGORIES, RecurringIntervals} from 'utils/constants';
import {findSelfHostedProductBySku} from 'utils/hosted_customer'; import {findSelfHostedProductBySku} from 'utils/hosted_customer';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
@@ -27,6 +27,7 @@ import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
import useCanSelfHostedSignup from 'components/common/hooks/useCanSelfHostedSignup'; import useCanSelfHostedSignup from 'components/common/hooks/useCanSelfHostedSignup';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import { import {
useControlAirGappedSelfHostedPurchaseModal, useControlAirGappedSelfHostedPurchaseModal,
@@ -89,6 +90,7 @@ function SelfHostedContent(props: ContentProps) {
const isEnterprise = license.SkuShortName === LicenseSkus.Enterprise; const isEnterprise = license.SkuShortName === LicenseSkus.Enterprise;
const isPostSelfHostedEnterpriseTrial = prevSelfHostedTrialLicense.IsLicensed === 'true'; const isPostSelfHostedEnterpriseTrial = prevSelfHostedTrialLicense.IsLicensed === 'true';
const [openContactSales] = useOpenSalesLink();
const controlScreeningInProgressModal = useControlScreeningInProgressModal(); const controlScreeningInProgressModal = useControlScreeningInProgressModal();
const controlAirgappedModal = useControlAirGappedSelfHostedPurchaseModal(); const controlAirgappedModal = useControlAirGappedSelfHostedPurchaseModal();
@@ -287,7 +289,7 @@ function SelfHostedContent(props: ContentProps) {
buttonDetails={(isPostSelfHostedEnterpriseTrial || !isAdmin) ? { buttonDetails={(isPostSelfHostedEnterpriseTrial || !isAdmin) ? {
action: () => { action: () => {
trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales'); trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales');
window.open(LicenseLinks.CONTACT_SALES, '_blank'); openContactSales();
}, },
text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}), text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}),
customClass: ButtonCustomiserClasses.active, customClass: ButtonCustomiserClasses.active,

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

@@ -19,7 +19,7 @@ import {GlobalState} from 'types/store';
import {BillingDetails} from 'types/cloud/sku'; import {BillingDetails} from 'types/cloud/sku';
import {isModalOpen} from 'selectors/views/modals'; import {isModalOpen} from 'selectors/views/modals';
import {getCloudContactUsLink, InquiryType, getCloudDelinquentInvoices, isCloudDelinquencyGreaterThan90Days} from 'selectors/cloud'; import {getCloudDelinquentInvoices, isCloudDelinquencyGreaterThan90Days} from 'selectors/cloud';
import {isDevModeEnabled} from 'selectors/general'; import {isDevModeEnabled} from 'selectors/general';
import {ModalIdentifiers} from 'utils/constants'; import {ModalIdentifiers} from 'utils/constants';
@@ -29,6 +29,7 @@ import {completeStripeAddPaymentMethod, subscribeCloudSubscription} from 'action
import {ModalData} from 'types/actions'; import {ModalData} from 'types/actions';
import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription'; import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription';
import {findOnlyYearlyProducts} from 'utils/products'; import {findOnlyYearlyProducts} from 'utils/products';
import {getCloudContactSalesLink, getCloudSupportLink} from 'utils/contact_support_sales';
const PurchaseModal = makeAsyncComponent('PurchaseModal', React.lazy(() => import('./purchase_modal'))); const PurchaseModal = makeAsyncComponent('PurchaseModal', React.lazy(() => import('./purchase_modal')));
@@ -39,19 +40,27 @@ function mapStateToProps(state: GlobalState) {
const products = state.entities.cloud!.products; const products = state.entities.cloud!.products;
const yearlyProducts = findOnlyYearlyProducts(products || {}); const yearlyProducts = findOnlyYearlyProducts(products || {});
const customer = state.entities.cloud.customer;
const customerEmail = customer?.email || '';
const firstName = customer?.contact_first_name || '';
const lastName = customer?.contact_last_name || '';
const companyName = customer?.name || '';
const contactSalesLink = getCloudContactSalesLink(firstName, lastName, companyName, customerEmail, 'mattermost', 'in-product-cloud');
const contactSupportLink = getCloudSupportLink(customerEmail, 'Cloud purchase', '', window.location.host);
return { return {
show: isModalOpen(state, ModalIdentifiers.CLOUD_PURCHASE), show: isModalOpen(state, ModalIdentifiers.CLOUD_PURCHASE),
products, products,
yearlyProducts, yearlyProducts,
isDevMode: isDevModeEnabled(state), isDevMode: isDevModeEnabled(state),
contactSupportLink: getCloudContactUsLink(state)(InquiryType.Technical), contactSupportLink,
invoices: getCloudDelinquentInvoices(state), invoices: getCloudDelinquentInvoices(state),
isCloudDelinquencyGreaterThan90Days: isCloudDelinquencyGreaterThan90Days(state), isCloudDelinquencyGreaterThan90Days: isCloudDelinquencyGreaterThan90Days(state),
isFreeTrial: subscription?.is_free_trial === 'true', isFreeTrial: subscription?.is_free_trial === 'true',
isComplianceBlocked: subscription?.compliance_blocked === 'true', isComplianceBlocked: subscription?.compliance_blocked === 'true',
contactSalesLink: getCloudContactUsLink(state)(InquiryType.Sales), contactSalesLink,
productId: subscription?.product_id, productId: subscription?.product_id,
customer: state.entities.cloud.customer, customer,
currentTeam: getCurrentTeam(state), currentTeam: getCurrentTeam(state),
theme: getTheme(state), theme: getTheme(state),
isDelinquencyModal, isDelinquencyModal,

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

@@ -2,42 +2,8 @@
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
.shipping-address-section { & &__purchase-body {
display: flex; overflow-y: auto;
align-content: center;
padding: 0 96px;
padding-bottom: 28px;
font-weight: normal;
button.no-style {
padding-left: 0;
background: transparent;
outline: unset;
&:focus {
outline: unset;
}
}
#address-same-than-billing-address {
width: 20px;
height: 20px;
margin-top: auto;
margin-bottom: auto;
}
.Form-checkbox-label {
padding-left: 12px;
cursor: default;
font-family: 'Open Sans', sans-serif;
vertical-align: middle;
}
.billing_address_btn_text {
color: var(--center-channel-color);
font-family: 'Open Sans', sans-serif;
font-weight: normal;
}
} }
>div { >div {

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

@@ -6,6 +6,7 @@
import React, {ReactNode} from 'react'; import React, {ReactNode} from 'react';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import classnames from 'classnames';
import {Stripe, StripeCardElementChangeEvent} from '@stripe/stripe-js'; import {Stripe, StripeCardElementChangeEvent} from '@stripe/stripe-js';
import {loadStripe} from '@stripe/stripe-js/pure'; // https://github.com/stripe/stripe-js#importing-loadstripe-without-side-effects import {loadStripe} from '@stripe/stripe-js/pure'; // https://github.com/stripe/stripe-js#importing-loadstripe-without-side-effects
import {Elements} from '@stripe/react-stripe-js'; import {Elements} from '@stripe/react-stripe-js';
@@ -17,7 +18,6 @@ import ComplianceScreenFailedSvg from 'components/common/svg_images_components/a
import AddressForm from 'components/payment_form/address_form'; import AddressForm from 'components/payment_form/address_form';
import {t} from 'utils/i18n'; import {t} from 'utils/i18n';
import {Address, CloudCustomer, Product, Invoice, areShippingDetailsValid, Feedback} from '@mattermost/types/cloud';
import {ActionResult} from 'mattermost-redux/types/actions'; import {ActionResult} from 'mattermost-redux/types/actions';
import {localizeMessage, getNextBillingDate, getBlankAddressWithCountry} from 'utils/utils'; import {localizeMessage, getNextBillingDate, getBlankAddressWithCountry} from 'utils/utils';
@@ -33,6 +33,7 @@ import {
ModalIdentifiers, ModalIdentifiers,
RecurringIntervals, RecurringIntervals,
} from 'utils/constants'; } from 'utils/constants';
import {goToMattermostContactSalesForm} from 'utils/contact_support_sales';
import PaymentDetails from 'components/admin_console/billing/payment_details'; import PaymentDetails from 'components/admin_console/billing/payment_details';
import {STRIPE_CSS_SRC, STRIPE_PUBLIC_KEY} from 'components/payment_form/stripe'; import {STRIPE_CSS_SRC, STRIPE_PUBLIC_KEY} from 'components/payment_form/stripe';
@@ -53,6 +54,8 @@ import {ModalData} from 'types/actions';
import {Theme} from 'mattermost-redux/selectors/entities/preferences'; import {Theme} from 'mattermost-redux/selectors/entities/preferences';
import {Address, CloudCustomer, Product, Invoice, areShippingDetailsValid, Feedback} from '@mattermost/types/cloud';
import {areBillingDetailsValid, BillingDetails} from '../../types/cloud/sku'; import {areBillingDetailsValid, BillingDetails} from '../../types/cloud/sku';
import {Team} from '@mattermost/types/teams'; import {Team} from '@mattermost/types/teams';
@@ -462,6 +465,7 @@ class PurchaseModal extends React.PureComponent<Props, State> {
} }
confirmSwitchToAnnual = () => { confirmSwitchToAnnual = () => {
const {customer} = this.props;
this.props.actions.openModal({ this.props.actions.openModal({
modalId: ModalIdentifiers.CONFIRM_SWITCH_TO_YEARLY, modalId: ModalIdentifiers.CONFIRM_SWITCH_TO_YEARLY,
dialogType: SwitchToYearlyPlanConfirmModal, dialogType: SwitchToYearlyPlanConfirmModal,
@@ -475,7 +479,11 @@ class PurchaseModal extends React.PureComponent<Props, State> {
TELEMETRY_CATEGORIES.CLOUD_ADMIN, TELEMETRY_CATEGORIES.CLOUD_ADMIN,
'confirm_switch_to_annual_click_contact_sales', 'confirm_switch_to_annual_click_contact_sales',
); );
window.open(this.props.contactSalesLink, '_blank'); const customerEmail = customer?.email || '';
const firstName = customer?.contact_first_name || '';
const lastName = customer?.contact_last_name || '';
const companyName = customer?.name || '';
goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, 'mattermost', 'in-product-cloud');
}, },
}, },
}); });
@@ -812,7 +820,7 @@ class PurchaseModal extends React.PureComponent<Props, State> {
} }
return ( return (
<div className={this.state.processing ? 'processing' : ''}> <div className={classnames('PurchaseModal__purchase-body', {processing: this.state.processing})}>
<div className='LHS'> <div className='LHS'>
<h2 className='title'>{title}</h2> <h2 className='title'>{title}</h2>
<UpgradeSvg <UpgradeSvg
@@ -1012,7 +1020,7 @@ class PurchaseModal extends React.PureComponent<Props, State> {
}); });
}} }}
contactSupportLink={ contactSupportLink={
this.props.contactSalesLink this.props.contactSupportLink
} }
currentTeam={this.props.currentTeam} currentTeam={this.props.currentTeam}
onSuccess={() => { onSuccess={() => {

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

@@ -0,0 +1,132 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import classNames from 'classnames';
import {COUNTRIES} from 'utils/countries';
import DropdownInput from 'components/dropdown_input';
import Input from 'components/widgets/inputs/input/input';
import StateSelector from 'components/payment_form/state_selector';
interface Props {
type: 'shipping' | 'billing';
testPrefix?: string;
country: string;
changeCountry: (option: {value: string}) => void;
address: string;
changeAddress: (e: React.ChangeEvent<HTMLInputElement>) => void;
address2: string;
changeAddress2: (e: React.ChangeEvent<HTMLInputElement>) => void;
city: string;
changeCity: (e: React.ChangeEvent<HTMLInputElement>) => void;
state: string;
changeState: (postalCode: string) => void;
postalCode: string;
changePostalCode: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export default function Address(props: Props) {
const testPrefix = props.testPrefix || 'selfHostedPurchase';
const intl = useIntl();
let countrySelectorId = `${testPrefix}CountrySelector`;
let stateSelectorId = `${testPrefix}StateSelector`;
if (props.type === 'shipping') {
countrySelectorId += '_Shipping';
stateSelectorId += '_Shipping';
}
return (
<>
<div className={classNames({'third-dropdown-sibling-wrapper': props.type === 'shipping'})}>
<DropdownInput
testId={countrySelectorId}
onChange={props.changeCountry}
value={
props.country ? {value: props.country, label: props.country} : undefined
}
options={COUNTRIES.map((country) => ({
value: country.name,
label: country.name,
}))}
legend={intl.formatMessage({
id: 'payment_form.country',
defaultMessage: 'Country',
})}
placeholder={intl.formatMessage({
id: 'payment_form.country',
defaultMessage: 'Country',
})}
name={'billing_dropdown'}
/>
</div>
<div className='form-row'>
<Input
name='address'
type='text'
value={props.address}
onChange={props.changeAddress}
placeholder={intl.formatMessage({
id: 'payment_form.address',
defaultMessage: 'Address',
})}
required={true}
/>
</div>
<div className='form-row'>
<Input
name='address2'
type='text'
value={props.address2}
onChange={props.changeAddress2}
placeholder={intl.formatMessage({
id: 'payment_form.address_2',
defaultMessage: 'Address 2',
})}
/>
</div>
<div className='form-row'>
<Input
name='city'
type='text'
value={props.city}
onChange={props.changeCity}
placeholder={intl.formatMessage({
id: 'payment_form.city',
defaultMessage: 'City',
})}
required={true}
/>
</div>
<div className='form-row'>
<div className={classNames('form-row-third-1', {'second-dropdown-sibling-wrapper': props.type === 'billing', 'fourth-dropdown-sibling-wrapper': props.type === 'shipping'})}>
<StateSelector
testId={stateSelectorId}
country={props.country}
state={props.state}
onChange={props.changeState}
/>
</div>
<div className='form-row-third-2'>
<Input
name='postalCode'
type='text'
value={props.postalCode}
onChange={props.changePostalCode}
placeholder={intl.formatMessage({
id: 'payment_form.zipcode',
defaultMessage: 'Zip/Postal Code',
})}
required={true}
/>
</div>
</div>
</>
);
}

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

@@ -4,18 +4,17 @@
import React from 'react'; import React from 'react';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
import {getCloudContactUsLink, InquiryType} from 'selectors/cloud';
import { import {
TELEMETRY_CATEGORIES, TELEMETRY_CATEGORIES,
} from 'utils/constants'; } from 'utils/constants';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
export default function ContactSalesLink() { export default function ContactSalesLink() {
const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical); const [, contactSalesLink] = useOpenSalesLink();
const intl = useIntl(); const intl = useIntl();
return ( return (
<ExternalLink <ExternalLink
@@ -26,7 +25,7 @@ export default function ContactSalesLink() {
'click_contact_sales', 'click_contact_sales',
); );
}} }}
href={contactSupportLink} href={contactSalesLink}
location='contact_sales_link' location='contact_sales_link'
> >
{intl.formatMessage({id: 'self_hosted_signup.contact_sales', defaultMessage: 'Contact Sales'})} {intl.formatMessage({id: 'self_hosted_signup.contact_sales', defaultMessage: 'Contact Sales'})}

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

@@ -4,13 +4,11 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import {getCloudContactUsLink, InquiryType} from 'selectors/cloud';
import PaymentFailedSvg from 'components/common/svg_images_components/payment_failed_svg'; import PaymentFailedSvg from 'components/common/svg_images_components/payment_failed_svg';
import AccessDeniedHappySvg from 'components/common/svg_images_components/access_denied_happy_svg'; import AccessDeniedHappySvg from 'components/common/svg_images_components/access_denied_happy_svg';
import IconMessage from 'components/purchase_modal/icon_message'; import IconMessage from 'components/purchase_modal/icon_message';
import {useOpenSelfHostedZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
interface Props { interface Props {
@@ -20,7 +18,7 @@ interface Props {
} }
export default function ErrorPage(props: Props) { export default function ErrorPage(props: Props) {
const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical); const [, contactSupportLink] = useOpenSelfHostedZendeskSupportForm('Purchase error');
let formattedTitle = ( let formattedTitle = (
<FormattedMessage <FormattedMessage
id='admin.billing.subscription.paymentVerificationFailed' id='admin.billing.subscription.paymentVerificationFailed'

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

@@ -310,6 +310,15 @@ describe('SelfHostedPurchaseModal :: canSubmit', () => {
state: 'string', state: 'string',
country: 'string', country: 'string',
postalCode: '12345', postalCode: '12345',
shippingSame: true,
shippingAddress: '',
shippingAddress2: '',
shippingCity: '',
shippingState: '',
shippingCountry: '',
shippingPostalCode: '',
cardName: 'string', cardName: 'string',
organization: 'string', organization: 'string',
agreedTerms: true, agreedTerms: true,
@@ -361,6 +370,21 @@ describe('SelfHostedPurchaseModal :: canSubmit', () => {
expect(canSubmit(state, SelfHostedSignupProgress.CREATED_CUSTOMER)).toBe(false); expect(canSubmit(state, SelfHostedSignupProgress.CREATED_CUSTOMER)).toBe(false);
expect(canSubmit(state, SelfHostedSignupProgress.CREATED_INTENT)).toBe(false); expect(canSubmit(state, SelfHostedSignupProgress.CREATED_INTENT)).toBe(false);
}); });
it('if shipping address different and is not filled, can not submit', () => {
const state = makeHappyPathState();
state.shippingSame = false;
expect(canSubmit(state, SelfHostedSignupProgress.START)).toBe(false);
state.shippingAddress = 'more shipping info';
state.shippingAddress2 = 'more shipping info';
state.shippingCity = 'more shipping info';
state.shippingState = 'more shipping info';
state.shippingCountry = 'more shipping info';
state.shippingPostalCode = 'more shipping info';
expect(canSubmit(state, SelfHostedSignupProgress.START)).toBe(true);
});
it('if card number missing and card has not been confirmed, can not submit', () => { it('if card number missing and card has not been confirmed, can not submit', () => {
const state = makeHappyPathState(); const state = makeHappyPathState();
state.cardFilled = false; state.cardFilled = false;

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

@@ -26,8 +26,6 @@ import {GlobalState} from 'types/store';
import {isModalOpen} from 'selectors/views/modals'; import {isModalOpen} from 'selectors/views/modals';
import {isDevModeEnabled} from 'selectors/general'; import {isDevModeEnabled} from 'selectors/general';
import {COUNTRIES} from 'utils/countries';
import { import {
ModalIdentifiers, ModalIdentifiers,
StatTypes, StatTypes,
@@ -35,8 +33,6 @@ import {
} from 'utils/constants'; } from 'utils/constants';
import CardInput, {CardInputType} from 'components/payment_form/card_input'; import CardInput, {CardInputType} from 'components/payment_form/card_input';
import StateSelector from 'components/payment_form/state_selector';
import DropdownInput from 'components/dropdown_input';
import BackgroundSvg from 'components/common/svg_images_components/background_svg'; import BackgroundSvg from 'components/common/svg_images_components/background_svg';
import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg'; import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg';
@@ -47,6 +43,7 @@ import RootPortal from 'components/root_portal';
import useLoadStripe from 'components/common/hooks/useLoadStripe'; import useLoadStripe from 'components/common/hooks/useLoadStripe';
import useControlSelfHostedPurchaseModal from 'components/common/hooks/useControlSelfHostedPurchaseModal'; import useControlSelfHostedPurchaseModal from 'components/common/hooks/useControlSelfHostedPurchaseModal';
import useFetchStandardAnalytics from 'components/common/hooks/useFetchStandardAnalytics'; import useFetchStandardAnalytics from 'components/common/hooks/useFetchStandardAnalytics';
import ChooseDifferentShipping from 'components/choose_different_shipping';
import {ValueOf} from '@mattermost/types/utilities'; import {ValueOf} from '@mattermost/types/utilities';
import {UserProfile} from '@mattermost/types/users'; import {UserProfile} from '@mattermost/types/users';
@@ -64,6 +61,7 @@ import SuccessPage from './success_page';
import SelfHostedCard from './self_hosted_card'; import SelfHostedCard from './self_hosted_card';
import StripeProvider from './stripe_provider'; import StripeProvider from './stripe_provider';
import Terms from './terms'; import Terms from './terms';
import Address from './address';
import useNoEscape from './useNoEscape'; import useNoEscape from './useNoEscape';
import {SetPrefix, UnionSetActions} from './types'; import {SetPrefix, UnionSetActions} from './types';
@@ -73,12 +71,24 @@ import './self_hosted_purchase_modal.scss';
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from './constants'; import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from './constants';
export interface State { export interface State {
// billing address
address: string; address: string;
address2: string; address2: string;
city: string; city: string;
state: string; state: string;
country: string; country: string;
postalCode: string; postalCode: string;
// shipping address
shippingSame: boolean;
shippingAddress: string;
shippingAddress2: string;
shippingCity: string;
shippingState: string;
shippingCountry: string;
shippingPostalCode: string;
cardName: string; cardName: string;
organization: string; organization: string;
agreedTerms: boolean; agreedTerms: boolean;
@@ -113,6 +123,15 @@ export function makeInitialState(): State {
state: '', state: '',
country: '', country: '',
postalCode: '', postalCode: '',
shippingSame: true,
shippingAddress: '',
shippingAddress2: '',
shippingCity: '',
shippingState: '',
shippingCountry: '',
shippingPostalCode: '',
cardName: '', cardName: '',
organization: '', organization: '',
agreedTerms: false, agreedTerms: false,
@@ -170,8 +189,18 @@ const simpleSetters: Array<Extract<keyof State, string>> = [
'address2', 'address2',
'city', 'city',
'country', 'country',
'postalCode',
'state', 'state',
'postalCode',
// shipping address
'shippingSame',
'shippingAddress',
'shippingAddress2',
'shippingCity',
'shippingState',
'shippingCountry',
'shippingPostalCode',
'agreedTerms', 'agreedTerms',
'cardFilled', 'cardFilled',
'cardName', 'cardName',
@@ -220,7 +249,7 @@ export function canSubmit(state: State, progress: ValueOf<typeof SelfHostedSignu
return false; return false;
} }
const validAddress = Boolean( let validAddress = Boolean(
state.organization && state.organization &&
state.address && state.address &&
state.city && state.city &&
@@ -228,6 +257,16 @@ export function canSubmit(state: State, progress: ValueOf<typeof SelfHostedSignu
state.postalCode && state.postalCode &&
state.country, state.country,
); );
if (!state.shippingSame) {
validAddress = validAddress && Boolean(
state.shippingAddress &&
state.shippingCity &&
state.shippingState &&
state.shippingPostalCode &&
state.shippingCountry,
);
}
const validCard = Boolean( const validCard = Boolean(
state.cardName && state.cardName &&
state.cardFilled, state.cardFilled,
@@ -366,16 +405,25 @@ export default function SelfHostedPurchaseModal(props: Props) {
try { try {
const [firstName, lastName] = inferNames(user, state.cardName); const [firstName, lastName] = inferNames(user, state.cardName);
const billingAddress = {
city: state.city,
country: state.country,
line1: state.address,
line2: state.address2,
postal_code: state.postalCode,
state: state.state,
};
signupCustomerResult = await Client4.createCustomerSelfHostedSignup({ signupCustomerResult = await Client4.createCustomerSelfHostedSignup({
first_name: firstName, first_name: firstName,
last_name: lastName, last_name: lastName,
billing_address: { billing_address: billingAddress,
city: state.city, shipping_address: state.shippingSame ? billingAddress : {
country: state.country, city: state.shippingCity,
line1: state.address, country: state.shippingCountry,
line2: state.address2, line1: state.shippingAddress,
postal_code: state.postalCode, line2: state.shippingAddress2,
state: state.state, postal_code: state.shippingPostalCode,
state: state.shippingState,
}, },
organization: state.organization, organization: state.organization,
}); });
@@ -586,99 +634,76 @@ export default function SelfHostedPurchaseModal(props: Props) {
defaultMessage='Billing address' defaultMessage='Billing address'
/> />
</div> </div>
<DropdownInput <Address
testId='selfHostedPurchaseCountrySelector' type='billing'
onChange={(option: {value: string}) => { country={state.country}
changeCountry={(option) => {
dispatch({type: 'set_country', data: option.value}); dispatch({type: 'set_country', data: option.value});
}} }}
value={ address={state.address}
state.country ? {value: state.country, label: state.country} : undefined changeAddress={(e) => {
} dispatch({type: 'set_address', data: e.target.value});
options={COUNTRIES.map((country) => ({ }}
value: country.name, address2={state.address2}
label: country.name, changeAddress2={(e) => {
}))} dispatch({type: 'set_address2', data: e.target.value});
legend={intl.formatMessage({ }}
id: 'payment_form.country', city={state.city}
defaultMessage: 'Country', changeCity={(e) => {
})} dispatch({type: 'set_city', data: e.target.value});
placeholder={intl.formatMessage({ }}
id: 'payment_form.country', state={state.state}
defaultMessage: 'Country', changeState={(state: string) => {
})} dispatch({type: 'set_state', data: state});
name={'billing_dropdown'} }}
postalCode={state.postalCode}
changePostalCode={(e) => {
dispatch({type: 'set_postalCode', data: e.target.value});
}}
/> />
<div className='form-row'> <ChooseDifferentShipping
<Input shippingIsSame={state.shippingSame}
name='address' setShippingIsSame={(val: boolean) => {
type='text' dispatch({type: 'set_shippingSame', data: val});
value={state.address} }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { />
dispatch({type: 'set_address', data: e.target.value}); {!state.shippingSame && (
}} <>
placeholder={intl.formatMessage({ <div className='section-title'>
id: 'payment_form.address', <FormattedMessage
defaultMessage: 'Address', id='payment_form.shipping_address'
})} defaultMessage='Shipping Address'
required={true} />
/> </div>
</div> <Address
<div className='form-row'> type='shipping'
<Input country={state.shippingCountry}
name='address2' changeCountry={(option) => {
type='text' dispatch({type: 'set_shippingCountry', data: option.value});
value={state.address2} }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { address={state.shippingAddress}
dispatch({type: 'set_address2', data: e.target.value}); changeAddress={(e) => {
}} dispatch({type: 'set_shippingAddress', data: e.target.value});
placeholder={intl.formatMessage({ }}
id: 'payment_form.address_2', address2={state.shippingAddress2}
defaultMessage: 'Address 2', changeAddress2={(e) => {
})} dispatch({type: 'set_shippingAddress2', data: e.target.value});
/> }}
</div> city={state.shippingCity}
<div className='form-row'> changeCity={(e) => {
<Input dispatch({type: 'set_shippingCity', data: e.target.value});
name='city' }}
type='text' state={state.shippingState}
value={state.city} changeState={(state: string) => {
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { dispatch({type: 'set_shippingState', data: state});
dispatch({type: 'set_city', data: e.target.value}); }}
}} postalCode={state.shippingPostalCode}
placeholder={intl.formatMessage({ changePostalCode={(e) => {
id: 'payment_form.city', dispatch({type: 'set_shippingPostalCode', data: e.target.value});
defaultMessage: 'City',
})}
required={true}
/>
</div>
<div className='form-row'>
<div className='form-row-third-1'>
<StateSelector
testId='selfHostedPurchaseStateSelector'
country={state.country}
state={state.state}
onChange={(state: string) => {
dispatch({type: 'set_state', data: state});
}} }}
/> />
</div> </>
<div className='form-row-third-2'> )}
<Input
name='postalCode'
type='text'
value={state.postalCode}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch({type: 'set_postalCode', data: e.target.value});
}}
placeholder={intl.formatMessage({
id: 'payment_form.zipcode',
defaultMessage: 'Zip/Postal Code',
})}
required={true}
/>
</div>
</div>
<Terms <Terms
agreed={state.agreedTerms} agreed={state.agreedTerms}
setAgreed={(data: boolean) => { setAgreed={(data: boolean) => {

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

@@ -4,19 +4,20 @@
.form-view { .form-view {
display: flex; display: flex;
overflow: hidden;
width: 100%; width: 100%;
height: 100%; height: 100%;
flex-direction: row; flex-direction: row;
flex-grow: 1; flex-grow: 1;
flex-wrap: wrap; flex-wrap: wrap;
align-content: top; align-items: flex-start;
justify-content: center; justify-content: center;
padding: 77px 107px; padding: 77px 107px;
color: var(--center-channel-color); color: var(--center-channel-color);
font-family: "Open Sans"; font-family: "Open Sans";
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
overflow-x: hidden;
overflow-y: auto;
.title { .title {
font-size: 22px; font-size: 22px;
@@ -39,14 +40,12 @@
margin-right: 16px; margin-right: 16px;
.DropdownInput { .DropdownInput {
z-index: 99999;
margin-top: 0; margin-top: 0;
} }
} }
.DropdownInput { .DropdownInput {
position: relative; position: relative;
z-index: 999999;
height: 36px; height: 36px;
margin-bottom: 24px; margin-bottom: 24px;
@@ -517,6 +516,9 @@
} }
input[type=checkbox] { input[type=checkbox] {
width: 17px;
height: 17px;
flex-shrink: 0;
margin-right: 12px; margin-right: 12px;
} }

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

@@ -16,7 +16,7 @@ import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {checkHadPriorTrial, isCurrentLicenseCloud, getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud'; import {checkHadPriorTrial, isCurrentLicenseCloud, getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {getBrowserTimezone} from 'utils/timezone'; import {getBrowserTimezone} from 'utils/timezone';
import {CloudProducts, LicenseLinks, LicenseSkus} from 'utils/constants'; import {CloudProducts, LicenseSkus} from 'utils/constants';
import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn'; import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn';
@@ -28,7 +28,7 @@ function ADLDAPUpsellBanner() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
const openSalesLink = useOpenSalesLink(); const [openSalesLink] = useOpenSalesLink();
useEffect(() => { useEffect(() => {
dispatch(getPrevTrialLicense()); dispatch(getPrevTrialLicense());
@@ -54,14 +54,6 @@ function ADLDAPUpsellBanner() {
const currentLicenseEndDate = new Date(parseInt(currentLicense?.ExpiresAt, 10)); const currentLicenseEndDate = new Date(parseInt(currentLicense?.ExpiresAt, 10));
const openLink = () => {
if (isCloud) {
openSalesLink();
} else {
window.open(LicenseLinks.CONTACT_SALES, '_blank');
}
};
const confirmBanner = ( const confirmBanner = (
<div className='ad_ldap_upsell_confirm'> <div className='ad_ldap_upsell_confirm'>
<div className='upsell-confirm-backdrop'/> <div className='upsell-confirm-backdrop'/>
@@ -71,7 +63,7 @@ function ADLDAPUpsellBanner() {
<div className='btns-container'> <div className='btns-container'>
<button <button
className='confrim-btn learn-more' className='confrim-btn learn-more'
onClick={openLink} onClick={openSalesLink}
> >
{formatMessage({id: 'adldap_upsell_banner.confirm.learn_more', defaultMessage: 'Learn more'})} {formatMessage({id: 'adldap_upsell_banner.confirm.learn_more', defaultMessage: 'Learn more'})}
</button> </button>
@@ -122,7 +114,7 @@ function ADLDAPUpsellBanner() {
btn = ( btn = (
<button <button
className='ad-ldap-banner-btn' className='ad-ldap-banner-btn'
onClick={openLink} onClick={openSalesLink}
> >
{formatMessage({id: 'adldap_upsell_banner.sales_btn', defaultMessage: 'Contact sales to use'})} {formatMessage({id: 'adldap_upsell_banner.sales_btn', defaultMessage: 'Contact sales to use'})}
</button> </button>

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

@@ -3484,8 +3484,6 @@
"modal.manual_status.title_offline": "Вашето състояние е зададено на \"Извън линия\"", "modal.manual_status.title_offline": "Вашето състояние е зададено на \"Извън линия\"",
"modal.manual_status.title_ooo": "Вашето състояние е зададено на \"Извън офиса\"", "modal.manual_status.title_ooo": "Вашето състояние е зададено на \"Извън офиса\"",
"more_channels.create": "Създайте канал", "more_channels.create": "Създайте канал",
"more_channels.createClick": "Кликнете върху \"Създаване на нов канал\", за да създадете канал",
"more_channels.joining": "Присъединявне ...",
"more_channels.next": "Следващ", "more_channels.next": "Следващ",
"more_channels.noMore": "Няма повече канали за присъединяване", "more_channels.noMore": "Няма повече канали за присъединяване",
"more_channels.prev": "Предишен", "more_channels.prev": "Предишен",

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

@@ -2127,7 +2127,7 @@
"admin.service.corsExposedHeadersTitle": "CORS-Exposed-Headers:", "admin.service.corsExposedHeadersTitle": "CORS-Exposed-Headers:",
"admin.service.corsHeadersEx": "X-My-Header", "admin.service.corsHeadersEx": "X-My-Header",
"admin.service.corsTitle": "Erlaube Cross Origin Requests von:", "admin.service.corsTitle": "Erlaube Cross Origin Requests von:",
"admin.service.developerDesc": "Wenn wahr, werden Javascript Fehler in einer roten Zeile im oberen Bereich des Interfaces angezeigt. Nicht empfohlen für Produktionsumgebungen. ", "admin.service.developerDesc": "Wenn wahr, werden Javascript Fehler in einer roten Zeile im oberen Bereich des Interfaces angezeigt. Nicht empfohlen für Produktionsumgebungen. Das Ändern dieser Einstellung erfordert einen Neustart des Servers, bevor sie wirksam wird.",
"admin.service.developerTitle": "Aktiviere Entwickler-Modus: ", "admin.service.developerTitle": "Aktiviere Entwickler-Modus: ",
"admin.service.disableBotOwnerDeactivatedTitle": "Deaktiviere Bot-Konten, wenn der Besitzer deaktiviert ist:", "admin.service.disableBotOwnerDeactivatedTitle": "Deaktiviere Bot-Konten, wenn der Besitzer deaktiviert ist:",
"admin.service.disableBotWhenOwnerIsDeactivated": "Wenn ein Benutzer deaktiviert ist, werden alle vom Benutzer verwalteten Bot-Konten deaktiviert. Um Bot-Konten wieder zu aktivieren, gehe zu [Integrationen > Bot-Konten]({siteURL}/_redirect/integrations/bots).", "admin.service.disableBotWhenOwnerIsDeactivated": "Wenn ein Benutzer deaktiviert ist, werden alle vom Benutzer verwalteten Bot-Konten deaktiviert. Um Bot-Konten wieder zu aktivieren, gehe zu [Integrationen > Bot-Konten]({siteURL}/_redirect/integrations/bots).",
@@ -3603,7 +3603,7 @@
"help.attaching.pasting.title": "Kopieren und Einfügen von Dateien", "help.attaching.pasting.title": "Kopieren und Einfügen von Dateien",
"help.attaching.previewer.description": "Mattermost verfügt über eine integrierte Dateivorschau, die zum Anzeigen von Medien, Herunterladen von Dateien und zum Teilen öffentlicher Links verwendet wird. Wähle die Miniaturansicht einer angehängten Datei, um sie in der Dateivorschau zu öffnen.", "help.attaching.previewer.description": "Mattermost verfügt über eine integrierte Dateivorschau, die zum Anzeigen von Medien, Herunterladen von Dateien und zum Teilen öffentlicher Links verwendet wird. Wähle die Miniaturansicht einer angehängten Datei, um sie in der Dateivorschau zu öffnen.",
"help.attaching.previewer.title": "Dateivorschau", "help.attaching.previewer.title": "Dateivorschau",
"help.attaching.publicLinks.description": "Mit öffentlichen Links kannst du Dateianhänge mit Personen außerhalb deines Mattermost-Teams teilen. Öffne die Dateivorschau, indem du die Miniaturansicht eines Anhangs auswählen, und wähle dann **Öffentlichen Link erhalten**. Kopiere den angegebenen Link. Wenn der Link freigegeben und von einem anderen Benutzer geöffnet wird, wird die Datei automatisch heruntergeladen.", "help.attaching.publicLinks.description": "Mit öffentlichen Links kannst du Dateianhänge mit Personen außerhalb deines Mattermost-Teams teilen. Öffne die Dateivorschau, indem du die Miniaturansicht eines Anhangs auswählen, und wähle dann **Öffentlichen Link erhalten**. Kopiere den angegebenen Link. Wenn der Link geteilt und von einem anderen Benutzer geöffnet wird, erfolgt ein automatischer Download der Datei.",
"help.attaching.publicLinks.title": "Links öffentlich teilen", "help.attaching.publicLinks.title": "Links öffentlich teilen",
"help.attaching.publicLinks2": "Wenn die Option **Öffentlichen Link abrufen** in der Dateivorschau nicht sichtbar ist und du diese Funktion aktivieren möchtest, bitten deinen Systemadministrator, diese Funktion in der Systemkonsole unter **Site-Konfiguration > Öffentliche Links** zu aktivieren.", "help.attaching.publicLinks2": "Wenn die Option **Öffentlichen Link abrufen** in der Dateivorschau nicht sichtbar ist und du diese Funktion aktivieren möchtest, bitten deinen Systemadministrator, diese Funktion in der Systemkonsole unter **Site-Konfiguration > Öffentliche Links** zu aktivieren.",
"help.attaching.supported.description": "Wenn du versuchst, eine Vorschau eines nicht unterstützten Medientyps anzuzeigen, öffnet die Dateivorschau ein Standardsymbol für Medienanhänge. Die unterstützten Medienformate hängen stark von deinem Browser und Betriebssystem ab. Die folgenden Formate werden von Mattermost in den meisten Browsern unterstützt:", "help.attaching.supported.description": "Wenn du versuchst, eine Vorschau eines nicht unterstützten Medientyps anzuzeigen, öffnet die Dateivorschau ein Standardsymbol für Medienanhänge. Die unterstützten Medienformate hängen stark von deinem Browser und Betriebssystem ab. Die folgenden Formate werden von Mattermost in den meisten Browsern unterstützt:",
@@ -4156,13 +4156,22 @@
"modal.manual_status.title_offline": "Dein Status wurde auf \"Offline\" gesetzt", "modal.manual_status.title_offline": "Dein Status wurde auf \"Offline\" gesetzt",
"modal.manual_status.title_ooo": "Dein Status ist auf \"Nicht im Büro\" gesetzt", "modal.manual_status.title_ooo": "Dein Status ist auf \"Nicht im Büro\" gesetzt",
"more.details": "Mehr Details", "more.details": "Mehr Details",
"more_channels.channel_purpose": "Kanal-Informationen: Mitgliedschaftsindikator: Beigetreten, Mitglieder {memberCount}, Zweck: {channelPurpose}",
"more_channels.count": "{count} Ergebnisse",
"more_channels.count_one": "1 Ergebnis",
"more_channels.count_zero": "Keine Ergebnisse",
"more_channels.create": "Kanal erstellen", "more_channels.create": "Kanal erstellen",
"more_channels.createClick": "Klicke auf 'Neuen Kanal erstellen' um einen Neuen zu erzeugen", "more_channels.hide_joined": "Verbundene Kanäle ausblenden",
"more_channels.join": "Beitreten", "more_channels.hide_joined_checked": "Kontrollkästchen Verbundene Kanäle ausblenden, aktiviert",
"more_channels.joining": "Betrete...", "more_channels.hide_joined_not_checked": "Kontrollkästchen Verbundene Kanäle ausblenden, deaktiviert",
"more_channels.joined": "Verknüpft",
"more_channels.membership_indicator": "Mitgliedschaftsindikator: Beigetreten",
"more_channels.next": "Weiter", "more_channels.next": "Weiter",
"more_channels.noMore": "Keine weiteren Kanäle, denen beigetreten werden kann", "more_channels.noArchived": "Keine archivierten Kanäle",
"more_channels.noMore": "Keine Ergebnisse für \"{text}\"",
"more_channels.noPublic": "Keine öffentlichen Kanäle",
"more_channels.prev": "Zurück", "more_channels.prev": "Zurück",
"more_channels.searchError": "Versuche, nach anderen Stichworten zu suchen, auf Tippfehlern zu prüfen oder die Filter anzupassen.",
"more_channels.show_archived_channels": "Anzeigen: Archivierte Kanäle", "more_channels.show_archived_channels": "Anzeigen: Archivierte Kanäle",
"more_channels.show_public_channels": "Anzeigen: Öffentliche Kanäle", "more_channels.show_public_channels": "Anzeigen: Öffentliche Kanäle",
"more_channels.title": "Weitere Kanäle", "more_channels.title": "Weitere Kanäle",
@@ -4372,6 +4381,7 @@
"payment_form.no_billing_address": "Keine Rechnungsadresse hinzugefügt", "payment_form.no_billing_address": "Keine Rechnungsadresse hinzugefügt",
"payment_form.no_credit_card": "Keine Kreditkarte hinzugefügt", "payment_form.no_credit_card": "Keine Kreditkarte hinzugefügt",
"payment_form.saved_payment_method": "Zahlungsmethode speichern", "payment_form.saved_payment_method": "Zahlungsmethode speichern",
"payment_form.shipping_address": "Lieferadresse",
"payment_form.zipcode": "Postleitzahl/Zip", "payment_form.zipcode": "Postleitzahl/Zip",
"pending_post_actions.cancel": "Abbrechen", "pending_post_actions.cancel": "Abbrechen",
"pending_post_actions.retry": "Erneut versuchen", "pending_post_actions.retry": "Erneut versuchen",

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

@@ -4378,6 +4378,7 @@
"payment_form.no_billing_address": "No billing address added", "payment_form.no_billing_address": "No billing address added",
"payment_form.no_credit_card": "No credit card added", "payment_form.no_credit_card": "No credit card added",
"payment_form.saved_payment_method": "Saved Payment Method", "payment_form.saved_payment_method": "Saved Payment Method",
"payment_form.shipping_address": "Shipping Address",
"payment_form.zipcode": "Zip/Postal Code", "payment_form.zipcode": "Zip/Postal Code",
"payment.card_number": "Card Number", "payment.card_number": "Card Number",
"payment.field_required": "This field is required", "payment.field_required": "This field is required",

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

@@ -4153,9 +4153,6 @@
"modal.manual_status.title_ooo": "Your Status is Set to 'Out of Office'", "modal.manual_status.title_ooo": "Your Status is Set to 'Out of Office'",
"more.details": "More details", "more.details": "More details",
"more_channels.create": "Create Channel", "more_channels.create": "Create Channel",
"more_channels.createClick": "Click 'Create New Channel' to make a new one",
"more_channels.join": "Join",
"more_channels.joining": "Joining...",
"more_channels.next": "Next", "more_channels.next": "Next",
"more_channels.noMore": "No more channels to join", "more_channels.noMore": "No more channels to join",
"more_channels.prev": "Previous", "more_channels.prev": "Previous",

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

@@ -255,6 +255,7 @@
"admin.billing.company_info_edit.sameAsBillingAddress": "Igual que la dirección de facturación", "admin.billing.company_info_edit.sameAsBillingAddress": "Igual que la dirección de facturación",
"admin.billing.company_info_edit.save": "Guardar información", "admin.billing.company_info_edit.save": "Guardar información",
"admin.billing.company_info_edit.title": "Editar información de la empresa", "admin.billing.company_info_edit.title": "Editar información de la empresa",
"admin.billing.deleteWorkspace.failureModal.buttonText": "Prueba de nuevo",
"admin.billing.history.allPaymentsShowHere": "Todos sus pagos mensuales se mostrarán aquí", "admin.billing.history.allPaymentsShowHere": "Todos sus pagos mensuales se mostrarán aquí",
"admin.billing.history.date": "Fecha", "admin.billing.history.date": "Fecha",
"admin.billing.history.description": "Descripción", "admin.billing.history.description": "Descripción",
@@ -3898,9 +3899,6 @@
"modal.manual_status.title_ooo": "Tu estado actual es \"Fuera de Oficina\"", "modal.manual_status.title_ooo": "Tu estado actual es \"Fuera de Oficina\"",
"more.details": "Más detalles", "more.details": "Más detalles",
"more_channels.create": "Crear Canal", "more_channels.create": "Crear Canal",
"more_channels.createClick": "Haz clic en 'Crear Nuevo Canal' para crear uno nuevo",
"more_channels.join": "Unirse",
"more_channels.joining": "Uniendo...",
"more_channels.next": "Siguiente", "more_channels.next": "Siguiente",
"more_channels.noMore": "No hay más canales para unirse", "more_channels.noMore": "No hay más canales para unirse",
"more_channels.prev": "Anterior", "more_channels.prev": "Anterior",

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

@@ -3707,9 +3707,6 @@
"modal.manual_status.title_ooo": "وضعیت شما روی \"خارج از دفتر\" تنظیم شده است", "modal.manual_status.title_ooo": "وضعیت شما روی \"خارج از دفتر\" تنظیم شده است",
"more.details": "جزئیات بیشتر", "more.details": "جزئیات بیشتر",
"more_channels.create": "ایجاد کانال", "more_channels.create": "ایجاد کانال",
"more_channels.createClick": "برای ایجاد کانال جدید روی \"ایجاد کانال جدید\" کلیک کنید",
"more_channels.join": "پیوستن",
"more_channels.joining": "پیوستن...",
"more_channels.next": "بعد", "more_channels.next": "بعد",
"more_channels.noMore": "کانال دیگری برای پیوستن وجود ندارد", "more_channels.noMore": "کانال دیگری برای پیوستن وجود ندارد",
"more_channels.prev": "قبلی", "more_channels.prev": "قبلی",

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

@@ -3736,9 +3736,6 @@
"modal.manual_status.title_offline": "Votre statut est défini sur « Hors ligne »", "modal.manual_status.title_offline": "Votre statut est défini sur « Hors ligne »",
"modal.manual_status.title_ooo": "Votre statut est défini sur « Absent du bureau »", "modal.manual_status.title_ooo": "Votre statut est défini sur « Absent du bureau »",
"more_channels.create": "Créer un canal", "more_channels.create": "Créer un canal",
"more_channels.createClick": "Veuillez cliquer sur « Créer un nouveau canal » pour en créer un nouveau",
"more_channels.join": "Rejoindre",
"more_channels.joining": "Accès en cours...",
"more_channels.next": "Suivant", "more_channels.next": "Suivant",
"more_channels.noMore": "Il n'y a plus d'autre canal que vous pouvez rejoindre", "more_channels.noMore": "Il n'y a plus d'autre canal que vous pouvez rejoindre",
"more_channels.prev": "Précédent", "more_channels.prev": "Précédent",

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

@@ -3920,9 +3920,6 @@
"modal.manual_status.title_ooo": "Az Ön állapota \"Irodán kívül\" -re van állítva", "modal.manual_status.title_ooo": "Az Ön állapota \"Irodán kívül\" -re van állítva",
"more.details": "További információ", "more.details": "További információ",
"more_channels.create": "Csatorna létrehozása", "more_channels.create": "Csatorna létrehozása",
"more_channels.createClick": "Kattintson az \"Új csatorna létrehozása\" gombra egy új létrehozásához",
"more_channels.join": "Csatlakozás",
"more_channels.joining": "Csatlakozás...",
"more_channels.next": "Következő", "more_channels.next": "Következő",
"more_channels.noMore": "Nincs több beszélgetés amelyhez csatlakozni lehetne", "more_channels.noMore": "Nincs több beszélgetés amelyhez csatlakozni lehetne",
"more_channels.prev": "Előző", "more_channels.prev": "Előző",

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

@@ -2997,8 +2997,6 @@
"modal.manual_status.title_offline": "Il tuo stato è \"Non in linea\"", "modal.manual_status.title_offline": "Il tuo stato è \"Non in linea\"",
"modal.manual_status.title_ooo": "Il tuo stato è \"Fuori sede\"", "modal.manual_status.title_ooo": "Il tuo stato è \"Fuori sede\"",
"more_channels.create": "Crea canale", "more_channels.create": "Crea canale",
"more_channels.createClick": "Click 'Crea un nuovo canale' per crearne uno nuovo",
"more_channels.joining": "Accoppiamento...",
"more_channels.next": "Prossimo", "more_channels.next": "Prossimo",
"more_channels.noMore": "Nessun altro canale in cui entrare", "more_channels.noMore": "Nessun altro canale in cui entrare",
"more_channels.prev": "Precedente", "more_channels.prev": "Precedente",

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

@@ -387,7 +387,7 @@
"admin.billing.subscription.privateCloudCard.contactSalesy": "営業担当に問い合わせる", "admin.billing.subscription.privateCloudCard.contactSalesy": "営業担当に問い合わせる",
"admin.billing.subscription.privateCloudCard.contactSupport": "サポートに連絡する", "admin.billing.subscription.privateCloudCard.contactSupport": "サポートに連絡する",
"admin.billing.subscription.privateCloudCard.freeTrial.description": "私たちは、お客様のニーズにお応えすることを大切にしています。サブスクリプション、請求書作成、トライアルに関するご質問は、営業までお問い合わせください。", "admin.billing.subscription.privateCloudCard.freeTrial.description": "私たちは、お客様のニーズにお応えすることを大切にしています。サブスクリプション、請求書作成、トライアルに関するご質問は、営業までお問い合わせください。",
"admin.billing.subscription.privateCloudCard.freeTrial.title": "トライアルに関する質問はこちら", "admin.billing.subscription.privateCloudCard.freeTrial.title": "トライアルに関する質問がありますか?",
"admin.billing.subscription.privateCloudCard.upgradeNow": "今すぐアップグレード", "admin.billing.subscription.privateCloudCard.upgradeNow": "今すぐアップグレード",
"admin.billing.subscription.proratedPayment.substitle": "{selectedProductName}にアップグレードしていただきありがとうございます。このプランのすべての機能にアクセスするには、数分後にワークスペースをチェックしてください。現在ご利用中の{currentProductName}プランと{selectedProductName}プランの料金については、請求サイクルの残り日数とユーザー数に応じた額を請求させていただきます。", "admin.billing.subscription.proratedPayment.substitle": "{selectedProductName}にアップグレードしていただきありがとうございます。このプランのすべての機能にアクセスするには、数分後にワークスペースをチェックしてください。現在ご利用中の{currentProductName}プランと{selectedProductName}プランの料金については、請求サイクルの残り日数とユーザー数に応じた額を請求させていただきます。",
"admin.billing.subscription.proratedPayment.title": "現在、{selectedProductName} を利用しています", "admin.billing.subscription.proratedPayment.title": "現在、{selectedProductName} を利用しています",
@@ -1989,6 +1989,8 @@
"admin.requestButton.requestFailure": "テストが失敗しました: {error}", "admin.requestButton.requestFailure": "テストが失敗しました: {error}",
"admin.requestButton.requestSuccess": "テストが成功しました", "admin.requestButton.requestSuccess": "テストが成功しました",
"admin.reset_email.cancel": "キャンセル", "admin.reset_email.cancel": "キャンセル",
"admin.reset_email.currentPassword": "現在のパスワード",
"admin.reset_email.missing_current_password": "現在のパスワードを入力してください。",
"admin.reset_email.newEmail": "新しい電子メールアドレス", "admin.reset_email.newEmail": "新しい電子メールアドレス",
"admin.reset_email.reset": "リセット", "admin.reset_email.reset": "リセット",
"admin.reset_email.titleReset": "電子メールを更新する", "admin.reset_email.titleReset": "電子メールを更新する",
@@ -2125,7 +2127,7 @@
"admin.service.corsExposedHeadersTitle": "CORS Exposedヘッダ:", "admin.service.corsExposedHeadersTitle": "CORS Exposedヘッダ:",
"admin.service.corsHeadersEx": "X-My-Header", "admin.service.corsHeadersEx": "X-My-Header",
"admin.service.corsTitle": "クロスオリジンリクエストを許可する:", "admin.service.corsTitle": "クロスオリジンリクエストを許可する:",
"admin.service.developerDesc": "有効な場合、JavaScriptのエラーはユーザーインターフェイス上部の紫色のバーに表示されます。本番環境での使用はお勧めできません。 ", "admin.service.developerDesc": "有効な場合、JavaScriptのエラーはユーザーインターフェイス上部の紫色のバーに表示されます。本番環境での使用はお勧めできません。",
"admin.service.developerTitle": "開発者モードを有効にする: ", "admin.service.developerTitle": "開発者モードを有効にする: ",
"admin.service.disableBotOwnerDeactivatedTitle": "オーナーが無効化された際にBotアカウントを無効化する:", "admin.service.disableBotOwnerDeactivatedTitle": "オーナーが無効化された際にBotアカウントを無効化する:",
"admin.service.disableBotWhenOwnerIsDeactivated": "ユーザーが無効化された際、そのユーザーが管理していたすべてのBotアカウントを無効化します。Botアカウントを再び有効にするには、[統合機能 > Botアカウント]({siteURL}/_redirect/integrations/bots) から設定してください。", "admin.service.disableBotWhenOwnerIsDeactivated": "ユーザーが無効化された際、そのユーザーが管理していたすべてのBotアカウントを無効化します。Botアカウントを再び有効にするには、[統合機能 > Botアカウント]({siteURL}/_redirect/integrations/bots) から設定してください。",
@@ -3128,7 +3130,7 @@
"create_post.deactivated": "**無効化されたユーザー** のいるアーカイブされたチャンネルを見ています。新しいメッセージは投稿できません。", "create_post.deactivated": "**無効化されたユーザー** のいるアーカイブされたチャンネルを見ています。新しいメッセージは投稿できません。",
"create_post.error_message": "メッセージが長すぎます。文字数: {length}/{limit}", "create_post.error_message": "メッセージが長すぎます。文字数: {length}/{limit}",
"create_post.fileProcessing": "処理しています...", "create_post.fileProcessing": "処理しています...",
"create_post.file_limit_sticky_banner.admin_message": "新たにアップロードすると古いファイルから自動的にアーカイブされます。古いファイルを削除するか、<a>有料プランにアップグレード</a>することで、再度表示できるようになります", "create_post.file_limit_sticky_banner.admin_message": "新たにアップロードすると古いファイルから自動的にアーカイブされます。古いファイルを削除するか、<a>有料プランにアップグレード</a>することで、再度表示できるようになります",
"create_post.file_limit_sticky_banner.messageTitle": "無料プランではファイル容量が {storageGB} に制限されます。", "create_post.file_limit_sticky_banner.messageTitle": "無料プランではファイル容量が {storageGB} に制限されます。",
"create_post.file_limit_sticky_banner.non_admin_message": "新たにアップロードすると古いファイルから自動的にアーカイブされます。再度表示するには、<a>管理者に有料プランへアップグレードするよう通知してください</a>。", "create_post.file_limit_sticky_banner.non_admin_message": "新たにアップロードすると古いファイルから自動的にアーカイブされます。再度表示するには、<a>管理者に有料プランへアップグレードするよう通知してください</a>。",
"create_post.file_limit_sticky_banner.snooze_tooltip": "{snoozeDays}日間スヌーズする", "create_post.file_limit_sticky_banner.snooze_tooltip": "{snoozeDays}日間スヌーズする",
@@ -4154,13 +4156,22 @@
"modal.manual_status.title_offline": "ステータスが \"オフライン\" になりました", "modal.manual_status.title_offline": "ステータスが \"オフライン\" になりました",
"modal.manual_status.title_ooo": "ステータスが \"外出中\" になりました", "modal.manual_status.title_ooo": "ステータスが \"外出中\" になりました",
"more.details": "もっと詳しく", "more.details": "もっと詳しく",
"more_channels.channel_purpose": "チャンネル情報: メンバーシップ状況: 加入済, メンバー数 {memberCount} , 目的: {channelPurpose}",
"more_channels.count": "{count}件",
"more_channels.count_one": "1件",
"more_channels.count_zero": "0件",
"more_channels.create": "チャンネルを作成する", "more_channels.create": "チャンネルを作成する",
"more_channels.createClick": "新しいチャンネルを作成するには「チャンネルを作成する」をクリックしてください", "more_channels.hide_joined": "参加したことを表示しない",
"more_channels.join": "参加", "more_channels.hide_joined_checked": "チャンネルに参加したことを表示しないチェックボックスがチェック済です",
"more_channels.joining": "参加しています....", "more_channels.hide_joined_not_checked": "チャンネルに参加したことを表示しないチェックボックスがチェックされていません",
"more_channels.joined": "参加済",
"more_channels.membership_indicator": "メンバーシップ状況: 参加済",
"more_channels.next": "次へ", "more_channels.next": "次へ",
"more_channels.noMore": "参加できるチャンネルありません", "more_channels.noArchived": "アーカイブされたチャンネルありません",
"more_channels.noMore": "\"{text}\"の結果はありません",
"more_channels.noPublic": "公開チャンネルはありません",
"more_channels.prev": "前へ", "more_channels.prev": "前へ",
"more_channels.searchError": "違うキーワードで検索してみたり、入力ミスを確認したり、フィルター設定を変更して再度お試しください。",
"more_channels.show_archived_channels": "表示: アーカイブチャンネル", "more_channels.show_archived_channels": "表示: アーカイブチャンネル",
"more_channels.show_public_channels": "表示: 公開チャンネル", "more_channels.show_public_channels": "表示: 公開チャンネル",
"more_channels.title": "他のチャンネル", "more_channels.title": "他のチャンネル",
@@ -4370,6 +4381,7 @@
"payment_form.no_billing_address": "請求先住所が追加されませんでした", "payment_form.no_billing_address": "請求先住所が追加されませんでした",
"payment_form.no_credit_card": "クレジットカードが追加されませんでした", "payment_form.no_credit_card": "クレジットカードが追加されませんでした",
"payment_form.saved_payment_method": "支払い方法を保存する", "payment_form.saved_payment_method": "支払い方法を保存する",
"payment_form.shipping_address": "配送先住所",
"payment_form.zipcode": "郵便番号", "payment_form.zipcode": "郵便番号",
"pending_post_actions.cancel": "キャンセル", "pending_post_actions.cancel": "キャンセル",
"pending_post_actions.retry": "再試行", "pending_post_actions.retry": "再試行",
@@ -4385,6 +4397,14 @@
"plan.self_serve": "セルフサービス", "plan.self_serve": "セルフサービス",
"pluggable.errorOccurred": "プラグイン {pluginId} でエラーが発生しました。", "pluggable.errorOccurred": "プラグイン {pluginId} でエラーが発生しました。",
"pluggable.errorRefresh": "更新しますか?", "pluggable.errorRefresh": "更新しますか?",
"pluggable_rhs.tourtip.boards.access": "右側のApp barの Boards アイコンから、リンクされた boards にアクセスできます。",
"pluggable_rhs.tourtip.boards.click": "この右側のパネルから boards をクリックします。",
"pluggable_rhs.tourtip.boards.review": "チャンネルから board の更新を確認します。",
"pluggable_rhs.tourtip.boards.title": "{count} 個のリンクされた {num, plural, one {board} other {boards}} にアクセスしましょう!",
"pluggable_rhs.tourtip.playbooks.access": "右側のApp barの Playbooks アイコンから、リンクされた playbooks にアクセスできます。",
"pluggable_rhs.tourtip.playbooks.click": "この右側のパネルから playbooks をクリックします。",
"pluggable_rhs.tourtip.playbooks.review": "チャンネルから playbook の更新を確認します。",
"pluggable_rhs.tourtip.playbooks.title": "{count} 個のリンクされた {num, plural, one {playbook} other {playbooks}} にアクセスしましょう。",
"post.ariaLabel.attachment": ", 1 添付ファイル", "post.ariaLabel.attachment": ", 1 添付ファイル",
"post.ariaLabel.attachmentMultiple": ", {attachmentCount} 添付ファイル", "post.ariaLabel.attachmentMultiple": ", {attachmentCount} 添付ファイル",
"post.ariaLabel.message": "{time} {date}, {authorName} が, {message} を書きました", "post.ariaLabel.message": "{time} {date}, {authorName} が, {message} を書きました",
@@ -4394,6 +4414,8 @@
"post.ariaLabel.reaction": ", 1 リアクション", "post.ariaLabel.reaction": ", 1 リアクション",
"post.ariaLabel.reactionMultiple": ", {reactionCount} リアクション", "post.ariaLabel.reactionMultiple": ", {reactionCount} リアクション",
"post.ariaLabel.replyMessage": "{time} {date}, {authorName} が, {message} と返信しました", "post.ariaLabel.replyMessage": "{time} {date}, {authorName} が, {message} と返信しました",
"post.reminder.acknowledgement": "{username} からのこのメッセージについて、{reminderDate}, {reminderTime} にリマインドされます: {permaLink}",
"post.reminder.systemBot": "{username} からのこのメッセージについてのリマインドです: {permaLink}",
"post_body.check_for_out_of_channel_groups_mentions.message": "彼らはチャンネルにいないため、このメンションによる通知は行われませんでした。また、彼らはリンクされたグループのメンバーではないため、チャンネルに追加することもできません。彼らをこのチャンネルに追加するには、リンクされたグループに追加しなければなりません。", "post_body.check_for_out_of_channel_groups_mentions.message": "彼らはチャンネルにいないため、このメンションによる通知は行われませんでした。また、彼らはリンクされたグループのメンバーではないため、チャンネルに追加することもできません。彼らをこのチャンネルに追加するには、リンクされたグループに追加しなければなりません。",
"post_body.check_for_out_of_channel_mentions.link.and": " と ", "post_body.check_for_out_of_channel_mentions.link.and": " と ",
"post_body.check_for_out_of_channel_mentions.link.private": "彼らを非公開チャンネルに追加しますか", "post_body.check_for_out_of_channel_mentions.link.private": "彼らを非公開チャンネルに追加しますか",
@@ -4434,6 +4456,13 @@
"post_info.message.visible.compact": " (あなただけが見ることができます)", "post_info.message.visible.compact": " (あなただけが見ることができます)",
"post_info.permalink": "リンクをコピーする", "post_info.permalink": "リンクをコピーする",
"post_info.pin": "チャンネルにピン留め", "post_info.pin": "チャンネルにピン留め",
"post_info.post_reminder.menu": "リマインダー",
"post_info.post_reminder.sub_menu.custom": "カスタム",
"post_info.post_reminder.sub_menu.header": "リマインダーを設定する:",
"post_info.post_reminder.sub_menu.one_hour": "1時間",
"post_info.post_reminder.sub_menu.thirty_minutes": "30分",
"post_info.post_reminder.sub_menu.tomorrow": "明日",
"post_info.post_reminder.sub_menu.two_hours": "2時間",
"post_info.reply": "返信する", "post_info.reply": "返信する",
"post_info.submenu.icon": "サブメニューアイコン", "post_info.submenu.icon": "サブメニューアイコン",
"post_info.submenu.mobile": "モバイルサブメニュー", "post_info.submenu.mobile": "モバイルサブメニュー",
@@ -4462,6 +4491,9 @@
"post_priority.requested_ack.description": "メッセージに確認ボタンが表示されます", "post_priority.requested_ack.description": "メッセージに確認ボタンが表示されます",
"post_priority.requested_ack.text": "確認を要求する", "post_priority.requested_ack.text": "確認を要求する",
"post_priority.you.acknowledge": "(あなた)", "post_priority.you.acknowledge": "(あなた)",
"post_reminder.custom_time_picker_modal.header": "リマインダーを設定する",
"post_reminder.custom_time_picker_modal.submit_button": "リマインダーを設定する",
"post_reminder_custom_time_picker_modal.defaultMsg": "リマインダーを設定する",
"postlist.toast.history": "メッセージの履歴を確認しています", "postlist.toast.history": "メッセージの履歴を確認しています",
"postlist.toast.newMessages": "新しい {count, number} {count, plural, one {メッセージ} other {メッセージ}}", "postlist.toast.newMessages": "新しい {count, number} {count, plural, one {メッセージ} other {メッセージ}}",
"postlist.toast.newMessagesSince": "{date} {isToday, select, true {} other {以降}} に投稿された新しい {count, number} {count, plural, one {メッセージ} other {メッセージ}}", "postlist.toast.newMessagesSince": "{date} {isToday, select, true {} other {以降}} に投稿された新しい {count, number} {count, plural, one {メッセージ} other {メッセージ}}",
@@ -5388,7 +5420,7 @@
"user.settings.notifications.email.disabled": "電子メール通知は有効化されていません", "user.settings.notifications.email.disabled": "電子メール通知は有効化されていません",
"user.settings.notifications.email.disabled_long": "電子メール通知はシステム管理者によって有効化されていません。", "user.settings.notifications.email.disabled_long": "電子メール通知はシステム管理者によって有効化されていません。",
"user.settings.notifications.email.everyHour": "1時間毎", "user.settings.notifications.email.everyHour": "1時間毎",
"user.settings.notifications.email.everyXMinutes": "{count}ごと", "user.settings.notifications.email.everyXMinutes": "{count, plural, one {分} other {{count, number} 分}}ごと",
"user.settings.notifications.email.immediately": "すぐに", "user.settings.notifications.email.immediately": "すぐに",
"user.settings.notifications.email.never": "通知しない", "user.settings.notifications.email.never": "通知しない",
"user.settings.notifications.email.send": "電子メール通知を送信する", "user.settings.notifications.email.send": "電子メール通知を送信する",

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

@@ -2883,8 +2883,6 @@
"modal.manual_status.title_offline": "상태가 \"오프라인\"이 되셨습니다", "modal.manual_status.title_offline": "상태가 \"오프라인\"이 되셨습니다",
"modal.manual_status.title_ooo": "상태가 \"오프라인\"이 되셨습니다", "modal.manual_status.title_ooo": "상태가 \"오프라인\"이 되셨습니다",
"more_channels.create": "채널 만들기", "more_channels.create": "채널 만들기",
"more_channels.createClick": "'새로 만들기'를 클릭하여 새로운 채널을 만드세요",
"more_channels.joining": "참가 중...",
"more_channels.next": "다음", "more_channels.next": "다음",
"more_channels.noMore": "가입할 수 있는 채널이 없습니다", "more_channels.noMore": "가입할 수 있는 채널이 없습니다",
"more_channels.prev": "이전", "more_channels.prev": "이전",

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

@@ -4155,9 +4155,6 @@
"modal.manual_status.title_ooo": "Je status is ingesteld op \"Out of Office\"", "modal.manual_status.title_ooo": "Je status is ingesteld op \"Out of Office\"",
"more.details": "Meer details", "more.details": "Meer details",
"more_channels.create": "Kanaal aanmaken", "more_channels.create": "Kanaal aanmaken",
"more_channels.createClick": "Klik 'Maak nieuw kanaal' om een nieuw kanaal te maken",
"more_channels.join": "Deelnemen",
"more_channels.joining": "Lid worden...",
"more_channels.next": "Volgende", "more_channels.next": "Volgende",
"more_channels.noMore": "Geen kanalen beschikbaar waar aan deelgenomen kan worden", "more_channels.noMore": "Geen kanalen beschikbaar waar aan deelgenomen kan worden",
"more_channels.prev": "Vorige", "more_channels.prev": "Vorige",

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

@@ -2127,7 +2127,7 @@
"admin.service.corsExposedHeadersTitle": "Eksponowane nagłówki CORS:", "admin.service.corsExposedHeadersTitle": "Eksponowane nagłówki CORS:",
"admin.service.corsHeadersEx": "X-Mój-Header", "admin.service.corsHeadersEx": "X-Mój-Header",
"admin.service.corsTitle": "Pozwól na zapytania Cross-domain z:", "admin.service.corsTitle": "Pozwól na zapytania Cross-domain z:",
"admin.service.developerDesc": "Gdy włączone, błędy JavaScript wyświetlane są na czerwonym pasku u góry interfejsu użytkownika. Nie zalecane w wersji produkcyjnej. ", "admin.service.developerDesc": "Gdy włączone, błędy JavaScript wyświetlane są na czerwonym pasku u góry interfejsu użytkownika. Nie zalecane w wersji produkcyjnej. Zmiana tego wymaga restartu serwera zanim zacznie działać.",
"admin.service.developerTitle": "Włączyć Tryb Dewelopera: ", "admin.service.developerTitle": "Włączyć Tryb Dewelopera: ",
"admin.service.disableBotOwnerDeactivatedTitle": "Wyłącz konta botów jeśli właściciel jest dezaktywowany:", "admin.service.disableBotOwnerDeactivatedTitle": "Wyłącz konta botów jeśli właściciel jest dezaktywowany:",
"admin.service.disableBotWhenOwnerIsDeactivated": "Kiedy użytkownik jest dezaktywowany, wyłącza wszystkie konta bot zarządzane przez użytkownika. Aby ponownie włączyć konta botów, przejdź do [Integracje > Konta Botów]({siteURL}/_redirect/integrations/bots).", "admin.service.disableBotWhenOwnerIsDeactivated": "Kiedy użytkownik jest dezaktywowany, wyłącza wszystkie konta bot zarządzane przez użytkownika. Aby ponownie włączyć konta botów, przejdź do [Integracje > Konta Botów]({siteURL}/_redirect/integrations/bots).",
@@ -4156,13 +4156,22 @@
"modal.manual_status.title_offline": "Twój status został ustawiony na \"Offline\"", "modal.manual_status.title_offline": "Twój status został ustawiony na \"Offline\"",
"modal.manual_status.title_ooo": "Twój status został ustawiony na \"Poza biurem\"", "modal.manual_status.title_ooo": "Twój status został ustawiony na \"Poza biurem\"",
"more.details": "Więcej informacji", "more.details": "Więcej informacji",
"more_channels.channel_purpose": "Informacje o kanale: Wskaźnik członkostwa: Dołączyło, liczba członków {memberCount}, Propozycje: {channelPurpose}",
"more_channels.count": "{count} Wyników",
"more_channels.count_one": "1 Wynik",
"more_channels.count_zero": "0 Wyników",
"more_channels.create": "Stwórz kanał", "more_channels.create": "Stwórz kanał",
"more_channels.createClick": "Kliknij przycisk 'Utwórz nowy kanał', aby dodać nowy", "more_channels.hide_joined": "Ukryj dołączonych",
"more_channels.join": "Dołącz do", "more_channels.hide_joined_checked": "Pole wyboru Ukryj połączone kanały, zaznaczone",
"more_channels.joining": "Dołączanie...", "more_channels.hide_joined_not_checked": "Pole wyboru Ukryj połączone kanały, nie zaznaczone",
"more_channels.joined": "Dołączył",
"more_channels.membership_indicator": "Wskaźnik członkostwa: Dołączył",
"more_channels.next": "Dalej", "more_channels.next": "Dalej",
"more_channels.noMore": "Brak kanałów", "more_channels.noArchived": "Brak zarchiwizowanych kanałów",
"more_channels.noMore": "Brak wyników dla \"{text}\"",
"more_channels.noPublic": "Brak kanałów publicznych",
"more_channels.prev": "Wstecz", "more_channels.prev": "Wstecz",
"more_channels.searchError": "Spróbuj wyszukać inne słowa kluczowe, sprawdzić literówki lub dostosować filtry.",
"more_channels.show_archived_channels": "Pokaż: Archiwizowane kanały", "more_channels.show_archived_channels": "Pokaż: Archiwizowane kanały",
"more_channels.show_public_channels": "Pokaż: Publiczne kanały", "more_channels.show_public_channels": "Pokaż: Publiczne kanały",
"more_channels.title": "Więcej Kanałów", "more_channels.title": "Więcej Kanałów",
@@ -4372,6 +4381,7 @@
"payment_form.no_billing_address": "Nie dodano adresu rozliczeniowego", "payment_form.no_billing_address": "Nie dodano adresu rozliczeniowego",
"payment_form.no_credit_card": "Nie dodano karty kredytowej", "payment_form.no_credit_card": "Nie dodano karty kredytowej",
"payment_form.saved_payment_method": "Zapisana Metoda Płatności", "payment_form.saved_payment_method": "Zapisana Metoda Płatności",
"payment_form.shipping_address": "Adres do wysyłki",
"payment_form.zipcode": "Kod Pocztowy", "payment_form.zipcode": "Kod Pocztowy",
"pending_post_actions.cancel": "Anuluj", "pending_post_actions.cancel": "Anuluj",
"pending_post_actions.retry": "Ponów", "pending_post_actions.retry": "Ponów",

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

@@ -3216,8 +3216,6 @@
"modal.manual_status.title_offline": "Seu Status está configurado para \"Desconectado\"", "modal.manual_status.title_offline": "Seu Status está configurado para \"Desconectado\"",
"modal.manual_status.title_ooo": "Seu Status está configurado para \"Fora do Escritório\"", "modal.manual_status.title_ooo": "Seu Status está configurado para \"Fora do Escritório\"",
"more_channels.create": "Criar Canal", "more_channels.create": "Criar Canal",
"more_channels.createClick": "Clique em 'Criar Novo Canal' para fazer um novo",
"more_channels.joining": "Juntando...",
"more_channels.next": "Próximo", "more_channels.next": "Próximo",
"more_channels.noMore": "Não há mais canais para participar", "more_channels.noMore": "Não há mais canais para participar",
"more_channels.prev": "Anterior", "more_channels.prev": "Anterior",

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

@@ -3306,8 +3306,6 @@
"modal.manual_status.title_offline": "Starea dvs. este setată la \"Offline\"", "modal.manual_status.title_offline": "Starea dvs. este setată la \"Offline\"",
"modal.manual_status.title_ooo": "Starea dvs. este setată la \"Plecat din birou\"", "modal.manual_status.title_ooo": "Starea dvs. este setată la \"Plecat din birou\"",
"more_channels.create": "Creați un nou canal", "more_channels.create": "Creați un nou canal",
"more_channels.createClick": "Dați clic pe \"Creați un nou canal\" pentru a crea unul nou",
"more_channels.joining": "Aderarea...",
"more_channels.next": "Următor", "more_channels.next": "Următor",
"more_channels.noMore": "Nu mai există canale care să se alăture", "more_channels.noMore": "Nu mai există canale care să se alăture",
"more_channels.prev": "Anterior", "more_channels.prev": "Anterior",

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

@@ -4157,9 +4157,6 @@
"modal.manual_status.title_ooo": "Ваш статус установлен на \"Не на работе\"", "modal.manual_status.title_ooo": "Ваш статус установлен на \"Не на работе\"",
"more.details": "Подробнее", "more.details": "Подробнее",
"more_channels.create": "Создать канал", "more_channels.create": "Создать канал",
"more_channels.createClick": "Нажмите 'Создать канал' для создания нового канала",
"more_channels.join": "Присоединиться",
"more_channels.joining": "Присоединяемся...",
"more_channels.next": "Далее", "more_channels.next": "Далее",
"more_channels.noMore": "Доступных каналов не найдено", "more_channels.noMore": "Доступных каналов не найдено",
"more_channels.prev": "Предыдущая", "more_channels.prev": "Предыдущая",

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

@@ -4157,9 +4157,6 @@
"modal.manual_status.title_ooo": "Din status är satt till \"Inte på kontoret\"", "modal.manual_status.title_ooo": "Din status är satt till \"Inte på kontoret\"",
"more.details": "Mer information", "more.details": "Mer information",
"more_channels.create": "Skapa kanal", "more_channels.create": "Skapa kanal",
"more_channels.createClick": "Tryck 'Skapa ny kanal' för att skapa en ny",
"more_channels.join": "Gå med",
"more_channels.joining": "Ansluter...",
"more_channels.next": "Nästa", "more_channels.next": "Nästa",
"more_channels.noMore": "Det finns inga fler kanaler att gå med i", "more_channels.noMore": "Det finns inga fler kanaler att gå med i",
"more_channels.prev": "Föregående", "more_channels.prev": "Föregående",

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