mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Merge branch 'master' of github.com:mattermost/mattermost-server into MM-50966-in-product-expansion-backend
This commit is contained in:
@@ -7,7 +7,7 @@ stages:
|
||||
|
||||
include:
|
||||
- project: mattermost/ci/mattermost-server
|
||||
ref: monorepo-testing
|
||||
ref: master
|
||||
file: private.yml
|
||||
|
||||
variables:
|
||||
|
||||
@@ -11,8 +11,8 @@ You may be licensed to use source code to create compiled versions not produced
|
||||
1. Under the Free Software Foundation’s 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
|
||||
|
||||
You are licensed to use the source code in Admin Tools and Configuration Files (templates/, config/default.json, i18n/, model/,
|
||||
plugin/ and all subdirectories thereof) under the Apache License v2.0.
|
||||
You are licensed to use the source code in Admin Tools and Configuration Files (server/templates/, server/i18n/, model/,
|
||||
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
|
||||
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.
|
||||
type SelfHostedCustomerForm struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
BillingAddress *Address `json:"billing_address"`
|
||||
Organization string `json:"organization"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
BillingAddress *Address `json:"billing_address"`
|
||||
ShippingAddress *Address `json:"shipping_address"`
|
||||
Organization string `json:"organization"`
|
||||
}
|
||||
|
||||
type SelfHostedConfirmPaymentMethodRequest struct {
|
||||
|
||||
@@ -140,6 +140,7 @@ func TestAddMemberToBoard(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPatchBoard(t *testing.T) {
|
||||
t.Skip("MM-51699")
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
|
||||
@@ -231,6 +231,9 @@ func (bm *BoardsMigrator) MigrateToStep(step int) error {
|
||||
func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor {
|
||||
return map[int]foundation.Interceptor{
|
||||
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 {
|
||||
query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " +
|
||||
"FROM " + s.tablePrefix + "category_boards) " +
|
||||
"DELETE " + s.tablePrefix + "category_boards FROM " + s.tablePrefix + "category_boards " +
|
||||
"JOIN duplicates USING(id) WHERE duplicates.rownum > 1;"
|
||||
query := "DELETE FROM " + s.tablePrefix + "category_boards WHERE id NOT IN " +
|
||||
"(SELECT * FROM ( SELECT min(id) FROM " + s.tablePrefix + "category_boards GROUP BY user_id, board_id ) as data)"
|
||||
if _, err := s.db.Exec(query); err != nil {
|
||||
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"testing"
|
||||
"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/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"
|
||||
}
|
||||
|
||||
func (th *TestHelper) F() *foundation.Foundation {
|
||||
return th.f
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T, f *foundation.Foundation) (*TestHelper, func()) {
|
||||
th := &TestHelper{t, f}
|
||||
|
||||
@@ -50,6 +50,7 @@ func NewStoreType(name string, driver string, skipMigrations bool) *storeType {
|
||||
DB: sqlDB,
|
||||
IsPlugin: false, // ToDo: to be removed
|
||||
}
|
||||
|
||||
store, err := New(storeParams)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("cannot create store: %s", err))
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
rudderKey = "placeholder_rudder_key"
|
||||
rudderKey = "placeholder_boards_rudder_key"
|
||||
rudderDataplaneURL = "placeholder_rudder_dataplane_url"
|
||||
timeBetweenTelemetryChecks = 10 * time.Minute
|
||||
)
|
||||
|
||||
@@ -71,10 +71,8 @@ type TestHelper struct {
|
||||
|
||||
IncludeCacheLayer bool
|
||||
|
||||
LogBuffer *mlog.Buffer
|
||||
TestLogger *mlog.Logger
|
||||
boardsProductEnvValue string
|
||||
playbooksDisableEnvValue string
|
||||
LogBuffer *mlog.Buffer
|
||||
TestLogger *mlog.Logger
|
||||
}
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
@@ -104,17 +102,6 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent
|
||||
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = 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 {
|
||||
updateConfig(memoryConfig)
|
||||
}
|
||||
@@ -153,15 +140,13 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent
|
||||
}
|
||||
|
||||
th := &TestHelper{
|
||||
App: app.New(app.ServerConnector(s.Channels())),
|
||||
Server: s,
|
||||
ConfigStore: configStore,
|
||||
IncludeCacheLayer: includeCache,
|
||||
Context: request.EmptyContext(testLogger),
|
||||
TestLogger: testLogger,
|
||||
LogBuffer: buffer,
|
||||
boardsProductEnvValue: boardsProductEnvValue,
|
||||
playbooksDisableEnvValue: playbooksDisableEnvValue,
|
||||
App: app.New(app.ServerConnector(s.Channels())),
|
||||
Server: s,
|
||||
ConfigStore: configStore,
|
||||
IncludeCacheLayer: includeCache,
|
||||
Context: request.EmptyContext(testLogger),
|
||||
TestLogger: testLogger,
|
||||
LogBuffer: buffer,
|
||||
}
|
||||
th.Context.SetLogger(testLogger)
|
||||
|
||||
@@ -386,17 +371,6 @@ func (th *TestHelper) ShutdownApp() {
|
||||
}
|
||||
|
||||
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 {
|
||||
// Clean all the caches
|
||||
th.App.Srv().InvalidateAllCaches()
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
|
||||
"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()
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
/* Temporarily comment out until MM-11108
|
||||
@@ -37,9 +39,26 @@ func init() {
|
||||
}
|
||||
|
||||
func TestUnitUpdateConfig(t *testing.T) {
|
||||
th := Setup(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
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
|
||||
|
||||
var called int32
|
||||
|
||||
@@ -42,9 +42,7 @@ type TestHelper struct {
|
||||
TestLogger *mlog.Logger
|
||||
IncludeCacheLayer bool
|
||||
|
||||
tempWorkspace string
|
||||
boardsProductEnvValue string
|
||||
playbooksDisableEnvValue string
|
||||
tempWorkspace string
|
||||
}
|
||||
|
||||
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.AnnouncementSettings.AdminNoticesEnabled = 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)
|
||||
|
||||
buffer := &mlog.Buffer{}
|
||||
@@ -103,14 +90,12 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
|
||||
}
|
||||
|
||||
th := &TestHelper{
|
||||
App: New(ServerConnector(s.Channels())),
|
||||
Context: request.EmptyContext(testLogger),
|
||||
Server: s,
|
||||
LogBuffer: buffer,
|
||||
TestLogger: testLogger,
|
||||
IncludeCacheLayer: includeCacheLayer,
|
||||
boardsProductEnvValue: boardsProductEnvValue,
|
||||
playbooksDisableEnvValue: playbooksDisableEnvValue,
|
||||
App: New(ServerConnector(s.Channels())),
|
||||
Context: request.EmptyContext(testLogger),
|
||||
Server: s,
|
||||
LogBuffer: buffer,
|
||||
TestLogger: testLogger,
|
||||
IncludeCacheLayer: includeCacheLayer,
|
||||
}
|
||||
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("UpdateLastActivityAt", "user1", mock.Anything).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.On("Close").Return(nil)
|
||||
emptyMockStore.On("Status").Return(&statusMock)
|
||||
emptyMockStore.On("Plugin").Return(&pluginMock).Maybe()
|
||||
th.App.Srv().SetStore(&emptyMockStore)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
@@ -553,17 +544,6 @@ func (th *TestHelper) ShutdownApp() {
|
||||
}
|
||||
|
||||
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 {
|
||||
// Clean all the caches
|
||||
th.App.Srv().InvalidateAllCaches()
|
||||
|
||||
@@ -1445,13 +1445,9 @@ func TestPushNotificationRace(t *testing.T) {
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
var err error
|
||||
s.platform, err = platform.New(
|
||||
platform.ServiceConfig{
|
||||
ConfigStore: memoryStore,
|
||||
},
|
||||
platform.SetFileStore(&fmocks.FileBackend{}),
|
||||
platform.StoreOverride(th.GetSqlStore()),
|
||||
)
|
||||
s.platform, err = platform.New(platform.ServiceConfig{
|
||||
ConfigStore: memoryStore,
|
||||
}, platform.SetFileStore(&fmocks.FileBackend{}))
|
||||
s.SetStore(mockStore)
|
||||
require.NoError(t, err)
|
||||
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 {
|
||||
ctx := request.EmptyContext(a.Srv().Log())
|
||||
currentSKU := "starter"
|
||||
license := a.Srv().License()
|
||||
if license == nil {
|
||||
return model.NewAppError("DoCheckForAdminNotifications", "app.notify_admin.send_notification_post.app_error", nil, "No license found", http.StatusInternalServerError)
|
||||
if license != nil {
|
||||
currentSKU = license.SkuShortName
|
||||
}
|
||||
|
||||
currentSKU := license.SkuShortName
|
||||
workspaceName := ""
|
||||
|
||||
return a.SendNotifyAdminPosts(ctx, workspaceName, currentSKU, trial)
|
||||
|
||||
@@ -73,17 +73,15 @@ func (s *Server) initializeProducts(
|
||||
func (s *Server) shouldStart(product string) bool {
|
||||
if product == "boards" {
|
||||
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
|
||||
}
|
||||
s.Log().Info("Allowing Boards init; enabled via feature flag")
|
||||
}
|
||||
if product == "playbooks" {
|
||||
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
|
||||
}
|
||||
s.Log().Info("Allowing Playbooks init; enabled via env var")
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -36,9 +36,7 @@ type TestHelper struct {
|
||||
TestLogger *mlog.Logger
|
||||
IncludeCacheLayer bool
|
||||
|
||||
tempWorkspace string
|
||||
boardsProductEnvValue string
|
||||
playbooksDisableEnvValue string
|
||||
tempWorkspace string
|
||||
}
|
||||
|
||||
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 {
|
||||
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.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
@@ -95,14 +82,12 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
|
||||
}
|
||||
|
||||
th := &TestHelper{
|
||||
App: app.New(app.ServerConnector(s.Channels())),
|
||||
Context: request.EmptyContext(testLogger),
|
||||
Server: s,
|
||||
LogBuffer: buffer,
|
||||
TestLogger: testLogger,
|
||||
IncludeCacheLayer: includeCacheLayer,
|
||||
boardsProductEnvValue: boardsProductEnvValue,
|
||||
playbooksDisableEnvValue: playbooksDisableEnvValue,
|
||||
App: app.New(app.ServerConnector(s.Channels())),
|
||||
Context: request.EmptyContext(testLogger),
|
||||
Server: s,
|
||||
LogBuffer: buffer,
|
||||
TestLogger: testLogger,
|
||||
IncludeCacheLayer: includeCacheLayer,
|
||||
}
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 })
|
||||
@@ -389,17 +374,6 @@ func (th *TestHelper) shutdownApp() {
|
||||
}
|
||||
|
||||
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 {
|
||||
// Clean all the caches
|
||||
th.App.Srv().InvalidateAllCaches()
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"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 {
|
||||
isEnabled := func(cfg *model.Config) bool {
|
||||
|
||||
@@ -48,9 +48,6 @@ type TestHelper struct {
|
||||
IncludeCacheLayer bool
|
||||
|
||||
TestLogger *mlog.Logger
|
||||
|
||||
boardsProductEnvValue string
|
||||
playbooksDisableEnvValue string
|
||||
}
|
||||
|
||||
func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
@@ -80,17 +77,6 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper {
|
||||
*newConfig.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*newConfig.AnnouncementSettings.UserNoticesEnabled = 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)
|
||||
var options []app.Option
|
||||
options = append(options, app.ConfigStore(memoryStore))
|
||||
@@ -148,14 +134,12 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper {
|
||||
})
|
||||
|
||||
th := &TestHelper{
|
||||
App: a,
|
||||
Context: request.EmptyContext(testLogger),
|
||||
Server: s,
|
||||
Web: web,
|
||||
IncludeCacheLayer: includeCacheLayer,
|
||||
TestLogger: testLogger,
|
||||
boardsProductEnvValue: boardsProductEnvValue,
|
||||
playbooksDisableEnvValue: playbooksDisableEnvValue,
|
||||
App: a,
|
||||
Context: request.EmptyContext(testLogger),
|
||||
Server: s,
|
||||
Web: web,
|
||||
IncludeCacheLayer: includeCacheLayer,
|
||||
TestLogger: testLogger,
|
||||
}
|
||||
th.Context.SetLogger(testLogger)
|
||||
|
||||
@@ -194,17 +178,6 @@ func (th *TestHelper) InitBasic() *TestHelper {
|
||||
}
|
||||
|
||||
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 {
|
||||
// Clean all the caches
|
||||
th.App.Srv().InvalidateAllCaches()
|
||||
|
||||
@@ -10013,5 +10013,213 @@
|
||||
{
|
||||
"id": "app.oauth.remove_auth_data_by_client_id.app_error",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
@@ -53,12 +54,10 @@ const (
|
||||
|
||||
const ServerKey product.ServiceKey = "server"
|
||||
|
||||
// These credentials for Rudder need to be populated at build-time,
|
||||
// passing the following flags to the go build command:
|
||||
// -ldflags "-X main.rudderDataplaneURL=<url> -X main.rudderWriteKey=<write_key>"
|
||||
var (
|
||||
rudderDataplaneURL string
|
||||
rudderWriteKey string
|
||||
// These credentials for Rudder need to be replaced at build-time.
|
||||
const (
|
||||
rudderDataplaneURL = "placeholder_rudder_dataplane_url"
|
||||
rudderWriteKey = "placeholder_playbooks_rudder_key"
|
||||
)
|
||||
|
||||
var errServiceTypeAssert = errors.New("type assertion failed")
|
||||
@@ -157,229 +156,9 @@ func newPlaybooksProduct(services map[product.ServiceKey]interface{}) (product.P
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger := logrus.StandardLogger()
|
||||
ConfigureLogrus(logger, playbooks.logger)
|
||||
|
||||
playbooks.server = services[ServerKey].(*mmapp.Server)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -531,6 +310,228 @@ func (pp *playbooksProduct) setProductServices(services map[product.ServiceKey]i
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("failed to register hooks: %w", err)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func TestActionCreation(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
createNewChannel := func(t *testing.T, name string) *model.Channel {
|
||||
@@ -201,8 +200,7 @@ func TestActionCreation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestActionList(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
// Create three valid actions
|
||||
@@ -294,8 +292,7 @@ func TestActionList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestActionUpdate(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
// Create a valid action
|
||||
|
||||
@@ -16,8 +16,7 @@ func TestTrialLicences(t *testing.T) {
|
||||
// This test is flaky due to upstream connectivity issues.
|
||||
t.Skip()
|
||||
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("request trial license without permissions", func(t *testing.T) {
|
||||
|
||||
@@ -11,8 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateClients()
|
||||
|
||||
t.Run("404", func(t *testing.T) {
|
||||
|
||||
@@ -21,8 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGraphQLPlaybooks(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("basic get", func(t *testing.T) {
|
||||
@@ -206,8 +205,7 @@ func TestGraphQLPlaybooks(t *testing.T) {
|
||||
|
||||
}
|
||||
func TestGraphQLUpdatePlaybookFails(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("metrics get", func(t *testing.T) {
|
||||
|
||||
@@ -20,8 +20,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGraphQLRunList(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("list by participantOrFollower", func(t *testing.T) {
|
||||
@@ -206,8 +205,7 @@ func TestGraphQLRunList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGraphQLChangeRunParticipants(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
user3, _, err := e.ServerAdminClient.CreateUser(&model.User{
|
||||
@@ -669,8 +667,7 @@ func TestGraphQLChangeRunParticipants(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGraphQLChangeRunOwner(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
// create a third user to test change owner
|
||||
@@ -713,8 +710,7 @@ func TestGraphQLChangeRunOwner(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetRunFavorite(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
createRun := func() *client.PlaybookRun {
|
||||
@@ -800,8 +796,7 @@ func TestSetRunFavorite(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolverFavorites(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
createRun := func() *client.PlaybookRun {
|
||||
@@ -833,8 +828,7 @@ func TestResolverFavorites(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolverPlaybooks(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
createRun := func() *client.PlaybookRun {
|
||||
@@ -860,8 +854,7 @@ func TestResolverPlaybooks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateRun(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
createRun := func() *client.PlaybookRun {
|
||||
@@ -977,8 +970,7 @@ func TestUpdateRun(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateRunTaskActions(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
testRunsQuery := `
|
||||
|
||||
@@ -22,8 +22,7 @@ import (
|
||||
)
|
||||
|
||||
func TestPlaybooks(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateClients()
|
||||
e.CreateBasicServer()
|
||||
|
||||
@@ -267,8 +266,7 @@ func TestPlaybooks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateInvalidPlaybook(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateClients()
|
||||
e.CreateBasicServer()
|
||||
|
||||
@@ -369,8 +367,7 @@ func TestCreateInvalidPlaybook(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybooksRetrieval(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("get playbook", func(t *testing.T) {
|
||||
@@ -387,8 +384,7 @@ func TestPlaybooksRetrieval(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybookUpdate(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("update playbook properties", func(t *testing.T) {
|
||||
@@ -521,8 +517,7 @@ func TestPlaybookUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybookUpdateCrossTeam(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateClients()
|
||||
e.CreateBasicServer()
|
||||
e.SetE20Licence()
|
||||
@@ -795,8 +789,7 @@ func TestPlaybooksSort(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybooksPaging(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateClients()
|
||||
e.CreateBasicServer()
|
||||
e.SetE20Licence()
|
||||
@@ -935,8 +928,7 @@ func getPlaybookIDsList(playbooks []client.Playbook) []string {
|
||||
}
|
||||
|
||||
func TestPlaybooksPermissions(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("public to private conversion", func(t *testing.T) {
|
||||
@@ -1208,8 +1199,7 @@ func TestPlaybooksConversions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybooksImportExport(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateClients()
|
||||
e.CreateBasicServer()
|
||||
e.CreateBasicPublicPlaybook()
|
||||
@@ -1237,8 +1227,7 @@ func TestPlaybooksImportExport(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybooksDuplicate(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateClients()
|
||||
e.CreateBasicServer()
|
||||
e.SetE20Licence()
|
||||
@@ -1259,8 +1248,7 @@ func TestPlaybooksDuplicate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddPostToTimeline(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
dialogRequest := model.SubmitDialogRequest{
|
||||
@@ -1307,8 +1295,7 @@ func TestAddPostToTimeline(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybookStats(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateClients()
|
||||
e.CreateBasicServer()
|
||||
e.SetE20Licence()
|
||||
@@ -1343,8 +1330,7 @@ func TestPlaybookStats(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybookGetAutoFollows(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
p1ID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{
|
||||
@@ -1450,8 +1436,7 @@ func TestPlaybookGetAutoFollows(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybookChecklistCleanup(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("update playbook", func(t *testing.T) {
|
||||
|
||||
@@ -19,8 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func TestRunCreation(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
incompletePlaybookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{
|
||||
@@ -314,8 +313,7 @@ func TestRunCreation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateRunInExistingChannel(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
// create playbook
|
||||
@@ -410,8 +408,7 @@ func TestCreateRunInExistingChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateInvalidRuns(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("by channel id", func(t *testing.T) {
|
||||
@@ -510,8 +506,7 @@ func TestRunRetrieval(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunPostStatusUpdate(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("post an update", func(t *testing.T) {
|
||||
@@ -571,8 +566,7 @@ func TestRunPostStatusUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChecklistManagement(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
createNewRunWithNoChecklists := func(t *testing.T) *client.PlaybookRun {
|
||||
@@ -1188,8 +1182,7 @@ func TestChecklistManagement(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChecklisFailTooLarge(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("public - get no updates", func(t *testing.T) {
|
||||
@@ -1343,8 +1335,7 @@ func TestRunGetStatusUpdates(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRequestUpdate(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("private - no viewer access ", func(t *testing.T) {
|
||||
@@ -1437,8 +1428,7 @@ func TestRequestUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReminderReset(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
ownerFromUser := func(u *model.User) client.OwnerInfo {
|
||||
|
||||
@@ -14,8 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSettings(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("get settings", func(t *testing.T) {
|
||||
|
||||
@@ -16,8 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGetSiteStats(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
t.Run("get sites stats", func(t *testing.T) {
|
||||
@@ -50,8 +49,7 @@ func TestGetSiteStats(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaybookKeyMetricsStats(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
|
||||
os.Unsetenv("MM_SERVICESETTINGS_LISTENADDRESS")
|
||||
@@ -126,11 +126,6 @@ func Setup(t *testing.T) (*TestEnvironment, func()) {
|
||||
config.LogSettings.EnableFile = model.NewBool(false)
|
||||
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
|
||||
textConfig, err := os.ReadFile("./e2etest.config.json")
|
||||
if err == nil {
|
||||
@@ -169,10 +164,6 @@ func Setup(t *testing.T) (*TestEnvironment, func()) {
|
||||
|
||||
ap := sapp.New(sapp.ServerConnector(server.Channels()))
|
||||
|
||||
teardown := func() {
|
||||
os.Setenv("MM_FEATUREFLAGS_BoardsProduct", boardsProductEnvValue)
|
||||
}
|
||||
|
||||
return &TestEnvironment{
|
||||
T: t,
|
||||
Srv: server,
|
||||
@@ -184,7 +175,7 @@ func Setup(t *testing.T) (*TestEnvironment, func()) {
|
||||
},
|
||||
},
|
||||
logger: testLogger,
|
||||
}, teardown
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
func TestTestFramework(t *testing.T) {
|
||||
e, teardown := Setup(t)
|
||||
defer teardown()
|
||||
e := Setup(t)
|
||||
e.CreateBasic()
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ function getSubpath(siteURL: string): string {
|
||||
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_OPTIONS = {
|
||||
context: {
|
||||
|
||||
@@ -2497,7 +2497,11 @@ const AdminDefinition = {
|
||||
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.NOTIFICATIONS)),
|
||||
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,
|
||||
|
||||
@@ -5,16 +5,12 @@ import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
type Props = {
|
||||
cancelAccountLink: any;
|
||||
}
|
||||
|
||||
const CancelSubscription = (props: Props) => {
|
||||
const {
|
||||
cancelAccountLink,
|
||||
} = props;
|
||||
const CancelSubscription = () => {
|
||||
const description = `I am requesting that workspace "${window.location.host}" be deleted`;
|
||||
const [, contactSupportURL] = useOpenCloudZendeskSupportForm('Request workspace be deleted', description);
|
||||
|
||||
return (
|
||||
<div className='cancelSubscriptionSection'>
|
||||
@@ -33,7 +29,7 @@ const CancelSubscription = (props: Props) => {
|
||||
</div>
|
||||
<ExternalLink
|
||||
location='cancel_subscription'
|
||||
href={cancelAccountLink}
|
||||
href={contactSupportURL}
|
||||
className='cancelSubscriptionSection__contactUs'
|
||||
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 './cloud_trial_banner.scss';
|
||||
import {SalesInquiryIssue} from 'selectors/cloud';
|
||||
|
||||
export interface Props {
|
||||
trialEndDate: number;
|
||||
@@ -34,7 +33,7 @@ const CloudTrialBanner = ({trialEndDate}: Props): JSX.Element | null => {
|
||||
const endDate = new Date(trialEndDate);
|
||||
const DISMISSED_DAYS = 10;
|
||||
const {formatMessage} = useIntl();
|
||||
const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise);
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector(getCurrentUser);
|
||||
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 CloudTrialSvg from 'components/common/svg_images_components/cloud_trial_svg';
|
||||
import {TelemetryProps} from 'components/common/hooks/useOpenPricingModal';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
type Props = {
|
||||
contactSalesLink: any;
|
||||
isFreeTrial: boolean;
|
||||
trialQuestionsLink: any;
|
||||
subscriptionPlan: string | undefined;
|
||||
onUpgradeMattermostCloud: (telemetryProps?: TelemetryProps | undefined) => void;
|
||||
}
|
||||
|
||||
const ContactSalesCard = (props: Props) => {
|
||||
const [openSalesLink, contactSalesLink] = useOpenSalesLink();
|
||||
const {
|
||||
contactSalesLink,
|
||||
isFreeTrial,
|
||||
trialQuestionsLink,
|
||||
subscriptionPlan,
|
||||
onUpgradeMattermostCloud,
|
||||
} = props;
|
||||
@@ -145,7 +143,7 @@ const ContactSalesCard = (props: Props) => {
|
||||
{(isFreeTrial || subscriptionPlan === CloudProducts.ENTERPRISE || isCloudLegacyPlan) &&
|
||||
<ExternalLink
|
||||
location='contact_sales_card'
|
||||
href={isFreeTrial ? trialQuestionsLink : contactSalesLink}
|
||||
href={contactSalesLink}
|
||||
className='PrivateCloudCard__actionButton'
|
||||
onClick={() => trackEvent('cloud_admin', 'click_contact_sales')}
|
||||
>
|
||||
@@ -163,7 +161,7 @@ const ContactSalesCard = (props: Props) => {
|
||||
if (subscriptionPlan === CloudProducts.STARTER) {
|
||||
onUpgradeMattermostCloud({trackingLocation: 'admin_console_subscription_card_upgrade_now_button'});
|
||||
} else {
|
||||
window.open(contactSalesLink, '_blank');
|
||||
openSalesLink();
|
||||
}
|
||||
}}
|
||||
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 CloudFetchError from 'components/cloud_fetch_error';
|
||||
|
||||
import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud';
|
||||
import {
|
||||
getSubscriptionProduct,
|
||||
getCloudSubscription as selectCloudSubscription,
|
||||
@@ -63,9 +62,6 @@ const BillingSubscriptions = () => {
|
||||
|
||||
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 [showCreditCardBanner, setShowCreditCardBanner] = useState(true);
|
||||
@@ -159,19 +155,12 @@ const BillingSubscriptions = () => {
|
||||
<Limits/>
|
||||
) : (
|
||||
<ContactSalesCard
|
||||
contactSalesLink={contactSalesLink}
|
||||
isFreeTrial={isFreeTrial}
|
||||
trialQuestionsLink={trialQuestionsLink}
|
||||
subscriptionPlan={product?.sku}
|
||||
onUpgradeMattermostCloud={openPricingModal}
|
||||
/>
|
||||
)}
|
||||
{isAnnualProfessionalOrEnterprise && !isFreeTrial ?
|
||||
<CancelSubscription
|
||||
cancelAccountLink={cancelAccountLink}
|
||||
/> :
|
||||
<DeleteWorkspaceCTA/>
|
||||
}
|
||||
{isAnnualProfessionalOrEnterprise && !isFreeTrial ? <CancelSubscription/> : <DeleteWorkspaceCTA/>}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('limits_reached_banner', () => {
|
||||
const store = mockStore(state);
|
||||
const spies = makeSpies();
|
||||
const mockOpenSalesLink = jest.fn();
|
||||
spies.useOpenSalesLink.mockReturnValue(mockOpenSalesLink);
|
||||
spies.useOpenSalesLink.mockReturnValue([mockOpenSalesLink, '']);
|
||||
spies.useGetUsageDeltas.mockReturnValue(someLimitReached);
|
||||
renderWithIntl(<Provider store={store}><LimitReachedBanner product={free}/></Provider>);
|
||||
screen.getByText(titleFree);
|
||||
|
||||
@@ -5,8 +5,6 @@ import React from 'react';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {SalesInquiryIssue} from 'selectors/cloud';
|
||||
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
import {anyUsageDeltaExceededLimit} from 'utils/limits';
|
||||
|
||||
@@ -33,7 +31,7 @@ const LimitReachedBanner = (props: Props) => {
|
||||
|
||||
const hasDismissedBanner = useSelector(getHasDismissedSystemConsoleLimitReached);
|
||||
|
||||
const openSalesLink = useOpenSalesLink(props.product?.sku === CloudProducts.PROFESSIONAL ? SalesInquiryIssue.UpgradeEnterprise : undefined);
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const openPricingModal = useOpenPricingModal();
|
||||
const saveBool = useSaveBool();
|
||||
if (hasDismissedBanner || !someLimitExceeded || !props.product || (props.product.sku !== CloudProducts.STARTER)) {
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
getSubscriptionProduct,
|
||||
} from 'mattermost-redux/selectors/entities/cloud';
|
||||
|
||||
import {SalesInquiryIssue} from 'selectors/cloud';
|
||||
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
import {asGBString, fallbackStarterLimits, hasSomeLimits} from 'utils/limits';
|
||||
|
||||
@@ -32,7 +30,7 @@ const Limits = (): JSX.Element | null => {
|
||||
const subscriptionProduct = useSelector(getSubscriptionProduct);
|
||||
const [cloudLimits, limitsLoaded] = useGetLimits();
|
||||
const usage = useGetUsage();
|
||||
const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise);
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const openPricingModal = useOpenPricingModal();
|
||||
|
||||
if (!subscriptionProduct || !limitsLoaded || !hasSomeLimits(cloudLimits)) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
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 {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
@@ -79,7 +78,7 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
const ToYearlyNudgeBanner = () => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const openSalesLink = useOpenSalesLink(SalesInquiryIssue.AboutPurchasing);
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import IconMessage from 'components/purchase_modal/icon_message';
|
||||
import FullScreenModal from 'components/widgets/modals/full_screen_modal';
|
||||
import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
|
||||
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
@@ -14,9 +15,6 @@ import {GlobalState} from 'types/store';
|
||||
|
||||
import './result_modal.scss';
|
||||
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import {InquiryType} from 'selectors/cloud';
|
||||
|
||||
type Props = {
|
||||
onHide?: () => void;
|
||||
icon: JSX.Element;
|
||||
@@ -33,7 +31,7 @@ type Props = {
|
||||
export default function ResultModal(props: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const openContactUs = useOpenSalesLink(undefined, InquiryType.Technical);
|
||||
const [openContactSupport] = useOpenCloudZendeskSupportForm('Delete workspace', '');
|
||||
|
||||
const isResultModalOpen = useSelector((state: GlobalState) =>
|
||||
isModalOpen(state, props.identifier),
|
||||
@@ -64,14 +62,13 @@ export default function ResultModal(props: Props) {
|
||||
buttonHandler={props.primaryButtonHandler}
|
||||
className={'success'}
|
||||
formattedTertiaryButonText={
|
||||
props.contactSupportButtonVisible ?
|
||||
props.contactSupportButtonVisible ? (
|
||||
<FormattedMessage
|
||||
id={'admin.billing.deleteWorkspace.resultModal.ContactSupport'}
|
||||
defaultMessage={'Contact Support'}
|
||||
/> :
|
||||
undefined
|
||||
/>) : undefined
|
||||
}
|
||||
tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactUs : undefined}
|
||||
tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactSupport : undefined}
|
||||
/>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
|
||||
@@ -17,7 +17,6 @@ describe('components/feature_discovery', () => {
|
||||
<FeatureDiscovery
|
||||
featureName='test'
|
||||
minimumSKURequiredForFeature={LicenseSkus.Professional}
|
||||
contactSalesLink='/sales'
|
||||
titleID='translation.test.title'
|
||||
titleDefault='Foo'
|
||||
copyID='translation.test.copy'
|
||||
@@ -46,7 +45,6 @@ describe('components/feature_discovery', () => {
|
||||
<FeatureDiscovery
|
||||
featureName='test'
|
||||
minimumSKURequiredForFeature={LicenseSkus.Professional}
|
||||
contactSalesLink='/sales'
|
||||
titleID='translation.test.title'
|
||||
titleDefault='Foo'
|
||||
copyID='translation.test.copy'
|
||||
@@ -76,7 +74,6 @@ describe('components/feature_discovery', () => {
|
||||
<FeatureDiscovery
|
||||
featureName='test'
|
||||
minimumSKURequiredForFeature={LicenseSkus.Professional}
|
||||
contactSalesLink='/sales'
|
||||
titleID='translation.test.title'
|
||||
titleDefault='Foo'
|
||||
copyID='translation.test.copy'
|
||||
|
||||
@@ -6,9 +6,6 @@ import {FormattedMessage} from 'react-intl';
|
||||
|
||||
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 AlertBanner from 'components/alert_banner';
|
||||
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 PurchaseModal from 'components/purchase_modal';
|
||||
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 {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
import {goToMattermostContactSalesForm} from 'utils/contact_support_sales';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_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 ExternalLink from 'components/external_link';
|
||||
|
||||
type Props = {
|
||||
featureName: string;
|
||||
@@ -56,7 +58,7 @@ type Props = {
|
||||
hadPrevCloudTrial: boolean;
|
||||
isSubscriptionLoaded: boolean;
|
||||
isPaidSubscription: boolean;
|
||||
contactSalesLink: string;
|
||||
customer?: CloudCustomer;
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
const {
|
||||
minimumSKURequiredForFeature,
|
||||
@@ -110,7 +122,7 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
data-testid='featureDiscovery_primaryCallToAction'
|
||||
onClick={() => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
|
||||
window.open(LicenseLinks.CONTACT_SALES, '_blank');
|
||||
this.contactSalesFunc();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -161,7 +173,6 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
hadPrevCloudTrial,
|
||||
isPaidSubscription,
|
||||
minimumSKURequiredForFeature,
|
||||
contactSalesLink,
|
||||
} = this.props;
|
||||
|
||||
const canRequestCloudFreeTrial = isCloud && !isCloudTrial && !hadPrevCloudTrial && !isPaidSubscription;
|
||||
@@ -217,11 +228,10 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
trackEvent(TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
|
||||
window.open(contactSalesLink, '_blank');
|
||||
} else {
|
||||
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
|
||||
window.open(LicenseLinks.CONTACT_SALES, '_blank');
|
||||
}
|
||||
this.contactSalesFunc();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -7,11 +7,9 @@ import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
import {getPrevTrialLicense} from 'mattermost-redux/actions/admin';
|
||||
import {getCloudSubscription} from 'mattermost-redux/actions/cloud';
|
||||
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 {getCloudContactUsLink, InquiryType} from 'selectors/cloud';
|
||||
|
||||
import {ModalData} from 'types/actions';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
@@ -30,7 +28,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
const isCloud = isCloudLicense(license);
|
||||
const hasPriorTrial = checkHadPriorTrial(state);
|
||||
const isCloudTrial = subscription?.is_free_trial === 'true';
|
||||
const contactSalesLink = getCloudContactUsLink(state)(InquiryType.Sales);
|
||||
const customer = getCloudCustomer(state);
|
||||
return {
|
||||
stats: state.entities.admin.analytics,
|
||||
prevTrialLicense: state.entities.admin.prevTrialLicense,
|
||||
@@ -39,7 +37,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
isSubscriptionLoaded: subscription !== undefined && subscription !== null,
|
||||
hadPrevCloudTrial: hasPriorTrial,
|
||||
isPaidSubscription: isCloud && license?.SkuShortName !== LicenseSkus.Starter && !isCloudTrial,
|
||||
contactSalesLink,
|
||||
customer,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,46 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {LicenseSkus} from 'utils/constants';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
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', () => {
|
||||
const license = {
|
||||
IsLicensed: 'true',
|
||||
@@ -28,8 +61,11 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
|
||||
} as EnterpriseEditionProps;
|
||||
|
||||
test('should render for no Gov no Trial no Enterprise', () => {
|
||||
const store = mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
<EnterpriseEditionRightPanel {...props}/>,
|
||||
<Provider store={store}>
|
||||
<EnterpriseEditionRightPanel {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
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', () => {
|
||||
const store = mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
<EnterpriseEditionRightPanel
|
||||
license={{...props.license, IsGovSku: 'true'}}
|
||||
isTrialLicense={props.isTrialLicense}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<EnterpriseEditionRightPanel
|
||||
license={{...props.license, IsGovSku: 'true'}}
|
||||
isTrialLicense={props.isTrialLicense}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
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', () => {
|
||||
const store = mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
<EnterpriseEditionRightPanel
|
||||
license={{...props.license, SkuShortName: LicenseSkus.Enterprise}}
|
||||
isTrialLicense={props.isTrialLicense}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<EnterpriseEditionRightPanel
|
||||
license={{...props.license, SkuShortName: LicenseSkus.Enterprise}}
|
||||
isTrialLicense={props.isTrialLicense}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
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', () => {
|
||||
const store = mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
<EnterpriseEditionRightPanel
|
||||
license={{...props.license, SkuShortName: LicenseSkus.E20}}
|
||||
isTrialLicense={props.isTrialLicense}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<EnterpriseEditionRightPanel
|
||||
license={{...props.license, SkuShortName: LicenseSkus.E20}}
|
||||
isTrialLicense={props.isTrialLicense}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
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', () => {
|
||||
const store = mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
<EnterpriseEditionRightPanel
|
||||
license={props.license}
|
||||
isTrialLicense={true}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<EnterpriseEditionRightPanel
|
||||
license={props.license}
|
||||
isTrialLicense={true}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
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', () => {
|
||||
const store = mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
<EnterpriseEditionRightPanel
|
||||
license={{...props.license, IsGovSku: 'true'}}
|
||||
isTrialLicense={true}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<EnterpriseEditionRightPanel
|
||||
license={{...props.license, IsGovSku: 'true'}}
|
||||
isTrialLicense={true}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
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';
|
||||
|
||||
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) =>
|
||||
act(
|
||||
() =>
|
||||
@@ -47,7 +78,7 @@ describe('components/RenewalLicenseCard', () => {
|
||||
});
|
||||
});
|
||||
getRenewalLinkSpy.mockImplementation(() => promise);
|
||||
const store = mockStore({});
|
||||
const store = mockStore(initialState);
|
||||
const wrapper = mountWithIntl(<Provider store={store}><RenewalLicenseCard {...props}/></Provider>);
|
||||
|
||||
// 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'));
|
||||
});
|
||||
getRenewalLinkSpy.mockImplementation(() => promise);
|
||||
const store = mockStore({});
|
||||
const store = mockStore(initialState);
|
||||
const wrapper = mountWithIntl(<Provider store={store}><RenewalLicenseCard {...props}/></Provider>);
|
||||
|
||||
// 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 {CloudLinks, ConsolePages, DocLinks, LicenseLinks} from 'utils/constants';
|
||||
import {CloudLinks, ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
|
||||
export type DataModel = {
|
||||
[key: string]: {
|
||||
@@ -84,8 +85,10 @@ const useMetricsData = () => {
|
||||
const isEnterpriseLicense = isEnterpriseOrE20License(license);
|
||||
const isStarterLicense = getIsStarterLicense(license);
|
||||
|
||||
const [, contactSalesLink] = useOpenSalesLink();
|
||||
|
||||
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'}),
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
|
||||
import './contact_us.scss';
|
||||
import {LicenseLinks} from '../../../utils/constants';
|
||||
|
||||
export interface Props {
|
||||
buttonTextElement?: JSX.Element;
|
||||
@@ -16,10 +17,12 @@ export interface Props {
|
||||
}
|
||||
|
||||
const ContactUsButton: React.FC<Props> = (props: Props) => {
|
||||
const [openContactSales] = useOpenSalesLink();
|
||||
|
||||
const handleContactUsLinkClick = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
trackEvent('admin', props.eventID || 'in_trial_contact_sales');
|
||||
window.open(LicenseLinks.CONTACT_SALES, '_blank');
|
||||
openContactSales();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,8 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
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';
|
||||
|
||||
@@ -34,6 +35,7 @@ const adminHasDismissed = ({preferenceName, overagePreferences, isWarningBanner}
|
||||
};
|
||||
|
||||
const OverageUsersBanner = () => {
|
||||
const [openContactSales] = useOpenSalesLink();
|
||||
const dispatch = useDispatch();
|
||||
const stats = useSelector((state: GlobalState) => state.entities.admin.analytics) || {};
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
@@ -90,7 +92,7 @@ const OverageUsersBanner = () => {
|
||||
const handleContactSalesClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
trackEventFn('Contact Sales');
|
||||
window.open(LicenseLinks.CONTACT_SALES, '_blank');
|
||||
openContactSales();
|
||||
};
|
||||
|
||||
const handleClick = isExpandable ? handleUpdateSeatsSelfServeClick : handleContactSalesClick;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {fireEvent, screen} from '@testing-library/react';
|
||||
import {DeepPartial} from '@mattermost/types/utilities';
|
||||
import {GlobalState} from 'types/store';
|
||||
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 {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
@@ -249,7 +249,10 @@ describe('components/overage_users_banner', () => {
|
||||
|
||||
fireEvent.click(screen.getByText(contactSalesTextLink));
|
||||
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).toBeCalledWith('insights', 'click_true_up_warning', {
|
||||
cta: 'Contact Sales',
|
||||
@@ -368,7 +371,10 @@ describe('components/overage_users_banner', () => {
|
||||
|
||||
fireEvent.click(screen.getByText(contactSalesTextLink));
|
||||
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).toBeCalledWith('insights', 'click_true_up_error', {
|
||||
cta: 'Contact Sales',
|
||||
|
||||
@@ -3,13 +3,46 @@
|
||||
|
||||
import React from 'react';
|
||||
import {ReactWrapper} from 'enzyme';
|
||||
import {Provider} from 'react-redux';
|
||||
import {act} from 'react-dom/test-utils';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
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) =>
|
||||
act(
|
||||
() =>
|
||||
@@ -40,7 +73,8 @@ describe('components/RenewalLink', () => {
|
||||
});
|
||||
});
|
||||
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
|
||||
await actImmediate(wrapper);
|
||||
@@ -54,7 +88,8 @@ describe('components/RenewalLink', () => {
|
||||
reject(new Error('License cannot be renewed from portal'));
|
||||
});
|
||||
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
|
||||
await actImmediate(wrapper);
|
||||
|
||||
@@ -11,9 +11,9 @@ import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {ModalData} from 'types/actions';
|
||||
|
||||
import {
|
||||
LicenseLinks,
|
||||
ModalIdentifiers,
|
||||
} from 'utils/constants';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
|
||||
import NoInternetConnection from '../no_internet_connection/no_internet_connection';
|
||||
|
||||
@@ -31,6 +31,9 @@ export interface RenewalLinkProps {
|
||||
const RenewalLink = (props: RenewalLinkProps) => {
|
||||
const [renewalLink, setRenewalLink] = useState('');
|
||||
const [manualInterventionRequired, setManualInterventionRequired] = useState(false);
|
||||
|
||||
const [openContactSales] = useOpenSalesLink();
|
||||
|
||||
useEffect(() => {
|
||||
Client4.getRenewalLink().then(({renewal_link: renewalLinkParam}) => {
|
||||
try {
|
||||
@@ -55,7 +58,7 @@ const RenewalLink = (props: RenewalLinkProps) => {
|
||||
}
|
||||
window.open(renewalLink, '_blank');
|
||||
} else if (manualInterventionRequired) {
|
||||
window.open(LicenseLinks.CONTACT_SALES, '_blank');
|
||||
openContactSales();
|
||||
} else {
|
||||
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 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 {ModalIdentifiers} from 'utils/constants';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
@@ -31,7 +30,8 @@ type Props = {
|
||||
function ErrorModal(props: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const subscriptionProduct = useSelector(getSubscriptionProduct);
|
||||
const openContactUs = useOpenSalesLink(undefined, InquiryType.Technical);
|
||||
|
||||
const [openContactSupport] = useOpenCloudZendeskSupportForm('Cloud Subscription', '');
|
||||
|
||||
const isSuccessModalOpen = useSelector((state: GlobalState) =>
|
||||
isModalOpen(state, ModalIdentifiers.ERROR_MODAL),
|
||||
@@ -97,7 +97,7 @@ function ErrorModal(props: Props) {
|
||||
}
|
||||
/>
|
||||
}
|
||||
tertiaryButtonHandler={openContactUs}
|
||||
tertiaryButtonHandler={openContactSupport}
|
||||
buttonHandler={onBackButtonPress}
|
||||
className={'success'}
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,35 @@
|
||||
|
||||
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) {
|
||||
const contactSalesLink = useSelector(getCloudContactUsLink)(inquireType, issue);
|
||||
export default function useOpenSalesLink(): [() => void, string] {
|
||||
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 {
|
||||
z-index: 999999;
|
||||
z-index: $dropdown_input_index;
|
||||
|
||||
&.Input_container {
|
||||
margin-top: 20px;
|
||||
@@ -37,7 +39,7 @@
|
||||
}
|
||||
|
||||
.DropdownInput__option > div {
|
||||
z-index: 999999;
|
||||
z-index: $dropdown_input_index;
|
||||
padding: 10px 24px;
|
||||
cursor: pointer;
|
||||
line-height: 16px;
|
||||
@@ -51,3 +53,33 @@
|
||||
.DropdownInput__option.focused > div {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<DropdownInput
|
||||
onChange={handleCountryChange}
|
||||
value={
|
||||
props.address.country ? {value: props.address.country, label: props.address.country} : undefined
|
||||
}
|
||||
options={COUNTRIES.map((country) => ({
|
||||
value: country.name,
|
||||
label: country.name,
|
||||
}))}
|
||||
legend={formatMessage({
|
||||
id: 'payment_form.country',
|
||||
defaultMessage: 'Country',
|
||||
})}
|
||||
placeholder={formatMessage({
|
||||
id: 'payment_form.country',
|
||||
defaultMessage: 'Country',
|
||||
})}
|
||||
name={'billing_dropdown'}
|
||||
/>
|
||||
<div className='third-dropdown-sibling-wrapper'>
|
||||
<DropdownInput
|
||||
onChange={handleCountryChange}
|
||||
value={
|
||||
props.address.country ? {value: props.address.country, label: props.address.country} : undefined
|
||||
}
|
||||
options={COUNTRIES.map((country) => ({
|
||||
value: country.name,
|
||||
label: country.name,
|
||||
}))}
|
||||
legend={formatMessage({
|
||||
id: 'payment_form.country',
|
||||
defaultMessage: 'Country',
|
||||
})}
|
||||
placeholder={formatMessage({
|
||||
id: 'payment_form.country',
|
||||
defaultMessage: 'Country',
|
||||
})}
|
||||
name={'billing_dropdown'}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-row'>
|
||||
<Input
|
||||
name='address'
|
||||
@@ -122,7 +124,7 @@ const AddressForm = (props: AddressFormProps) => {
|
||||
/>
|
||||
</div>
|
||||
<div className='form-row'>
|
||||
<div className='form-row-third-1 selector'>
|
||||
<div className='form-row-third-1 selector fourth-dropdown-sibling-wrapper'>
|
||||
<StateSelector
|
||||
country={props.address.country}
|
||||
state={props.address.state}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
.form-row-third-1 {
|
||||
.DropdownInput {
|
||||
z-index: 99999;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -37,7 +36,6 @@
|
||||
|
||||
.DropdownInput {
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
height: 36px;
|
||||
margin-bottom: 24px;
|
||||
font-weight: normal;
|
||||
|
||||
@@ -251,7 +251,7 @@ export default class PaymentForm extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
</div>
|
||||
<div className='form-row'>
|
||||
<div className='form-row-third-1 selector'>
|
||||
<div className='form-row-third-1 selector second-dropdown-sibling-wrapper'>
|
||||
<StateSelector
|
||||
country={this.state.country}
|
||||
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 (
|
||||
<Picker
|
||||
|
||||
@@ -11,8 +11,7 @@ import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
|
||||
import {LicenseLinks, TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
import {SalesInquiryIssue} from 'selectors/cloud';
|
||||
import {TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
|
||||
const StyledA = styled.a`
|
||||
color: var(--denim-button-bg);
|
||||
@@ -27,11 +26,7 @@ text-align: center;
|
||||
|
||||
function ContactSalesCTA() {
|
||||
const {formatMessage} = useIntl();
|
||||
const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise);
|
||||
|
||||
const openSelfHostedLink = () => {
|
||||
window.open(LicenseLinks.CONTACT_SALES, '_blank');
|
||||
};
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
|
||||
const isCloud = useSelector(isCurrentLicenseCloud);
|
||||
|
||||
@@ -42,11 +37,10 @@ function ContactSalesCTA() {
|
||||
e.preventDefault();
|
||||
if (isCloud) {
|
||||
trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales');
|
||||
openSalesLink();
|
||||
} else {
|
||||
trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales');
|
||||
openSelfHostedLink();
|
||||
}
|
||||
openSalesLink();
|
||||
}}
|
||||
>
|
||||
{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 {findOnlyYearlyProducts, findProductBySku} from 'utils/products';
|
||||
|
||||
import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {closeModal, openModal} from 'actions/views/modals';
|
||||
import {subscribeCloudSubscription} from 'actions/cloud';
|
||||
@@ -38,6 +36,8 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch
|
||||
|
||||
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
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 DowngradeTeamRemovalModal from './downgrade_team_removal_modal';
|
||||
@@ -64,15 +64,12 @@ function Content(props: ContentProps) {
|
||||
const openPricingModalBackAction = useOpenPricingModal();
|
||||
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
const contactSalesLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.UpgradeEnterprise);
|
||||
|
||||
const subscription = useSelector(selectCloudSubscription);
|
||||
const currentProduct = useSelector(selectSubscriptionProduct);
|
||||
const products = useSelector(selectCloudProducts);
|
||||
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 isEnterprise = currentProduct?.sku === CloudProducts.ENTERPRISE;
|
||||
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 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 openCloudDelinquencyModal = useOpenCloudPurchaseModal({
|
||||
isDelinquencyModal: true,
|
||||
@@ -239,7 +238,7 @@ function Content(props: ContentProps) {
|
||||
return {
|
||||
action: () => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales');
|
||||
window.open(contactSalesLink, '_blank');
|
||||
openContactSales();
|
||||
},
|
||||
text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}),
|
||||
customClass: ButtonCustomiserClasses.active,
|
||||
@@ -350,7 +349,7 @@ function Content(props: ContentProps) {
|
||||
buttonDetails={{
|
||||
action: () => {
|
||||
if (!isStarter && !currentSubscriptionIsMonthly) {
|
||||
window.open(contactSupportLink, '_blank');
|
||||
openContactSupport();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Modal} from 'react-bootstrap';
|
||||
import {useIntl} from 'react-intl';
|
||||
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 {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 useCanSelfHostedSignup from 'components/common/hooks/useCanSelfHostedSignup';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
|
||||
import {
|
||||
useControlAirGappedSelfHostedPurchaseModal,
|
||||
@@ -89,6 +90,7 @@ function SelfHostedContent(props: ContentProps) {
|
||||
const isEnterprise = license.SkuShortName === LicenseSkus.Enterprise;
|
||||
const isPostSelfHostedEnterpriseTrial = prevSelfHostedTrialLicense.IsLicensed === 'true';
|
||||
|
||||
const [openContactSales] = useOpenSalesLink();
|
||||
const controlScreeningInProgressModal = useControlScreeningInProgressModal();
|
||||
const controlAirgappedModal = useControlAirGappedSelfHostedPurchaseModal();
|
||||
|
||||
@@ -287,7 +289,7 @@ function SelfHostedContent(props: ContentProps) {
|
||||
buttonDetails={(isPostSelfHostedEnterpriseTrial || !isAdmin) ? {
|
||||
action: () => {
|
||||
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'}),
|
||||
customClass: ButtonCustomiserClasses.active,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {GlobalState} from 'types/store';
|
||||
import {BillingDetails} from 'types/cloud/sku';
|
||||
|
||||
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 {ModalIdentifiers} from 'utils/constants';
|
||||
@@ -29,6 +29,7 @@ import {completeStripeAddPaymentMethod, subscribeCloudSubscription} from 'action
|
||||
import {ModalData} from 'types/actions';
|
||||
import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription';
|
||||
import {findOnlyYearlyProducts} from 'utils/products';
|
||||
import {getCloudContactSalesLink, getCloudSupportLink} from 'utils/contact_support_sales';
|
||||
|
||||
const PurchaseModal = makeAsyncComponent('PurchaseModal', React.lazy(() => import('./purchase_modal')));
|
||||
|
||||
@@ -39,19 +40,27 @@ function mapStateToProps(state: GlobalState) {
|
||||
const products = state.entities.cloud!.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 {
|
||||
show: isModalOpen(state, ModalIdentifiers.CLOUD_PURCHASE),
|
||||
products,
|
||||
yearlyProducts,
|
||||
isDevMode: isDevModeEnabled(state),
|
||||
contactSupportLink: getCloudContactUsLink(state)(InquiryType.Technical),
|
||||
contactSupportLink,
|
||||
invoices: getCloudDelinquentInvoices(state),
|
||||
isCloudDelinquencyGreaterThan90Days: isCloudDelinquencyGreaterThan90Days(state),
|
||||
isFreeTrial: subscription?.is_free_trial === 'true',
|
||||
isComplianceBlocked: subscription?.compliance_blocked === 'true',
|
||||
contactSalesLink: getCloudContactUsLink(state)(InquiryType.Sales),
|
||||
contactSalesLink,
|
||||
productId: subscription?.product_id,
|
||||
customer: state.entities.cloud.customer,
|
||||
customer,
|
||||
currentTeam: getCurrentTeam(state),
|
||||
theme: getTheme(state),
|
||||
isDelinquencyModal,
|
||||
|
||||
@@ -2,42 +2,8 @@
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.shipping-address-section {
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
& &__purchase-body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
>div {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import React, {ReactNode} from 'react';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
|
||||
import classnames from 'classnames';
|
||||
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 {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 {t} from 'utils/i18n';
|
||||
import {Address, CloudCustomer, Product, Invoice, areShippingDetailsValid, Feedback} from '@mattermost/types/cloud';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {localizeMessage, getNextBillingDate, getBlankAddressWithCountry} from 'utils/utils';
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
ModalIdentifiers,
|
||||
RecurringIntervals,
|
||||
} from 'utils/constants';
|
||||
import {goToMattermostContactSalesForm} from 'utils/contact_support_sales';
|
||||
|
||||
import PaymentDetails from 'components/admin_console/billing/payment_details';
|
||||
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 {Address, CloudCustomer, Product, Invoice, areShippingDetailsValid, Feedback} from '@mattermost/types/cloud';
|
||||
|
||||
import {areBillingDetailsValid, BillingDetails} from '../../types/cloud/sku';
|
||||
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
@@ -462,6 +465,7 @@ class PurchaseModal extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
confirmSwitchToAnnual = () => {
|
||||
const {customer} = this.props;
|
||||
this.props.actions.openModal({
|
||||
modalId: ModalIdentifiers.CONFIRM_SWITCH_TO_YEARLY,
|
||||
dialogType: SwitchToYearlyPlanConfirmModal,
|
||||
@@ -475,7 +479,11 @@ class PurchaseModal extends React.PureComponent<Props, State> {
|
||||
TELEMETRY_CATEGORIES.CLOUD_ADMIN,
|
||||
'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 (
|
||||
<div className={this.state.processing ? 'processing' : ''}>
|
||||
<div className={classnames('PurchaseModal__purchase-body', {processing: this.state.processing})}>
|
||||
<div className='LHS'>
|
||||
<h2 className='title'>{title}</h2>
|
||||
<UpgradeSvg
|
||||
@@ -1012,7 +1020,7 @@ class PurchaseModal extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}}
|
||||
contactSupportLink={
|
||||
this.props.contactSalesLink
|
||||
this.props.contactSupportLink
|
||||
}
|
||||
currentTeam={this.props.currentTeam}
|
||||
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 {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import {getCloudContactUsLink, InquiryType} from 'selectors/cloud';
|
||||
import {
|
||||
TELEMETRY_CATEGORIES,
|
||||
} from 'utils/constants';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
export default function ContactSalesLink() {
|
||||
const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical);
|
||||
const [, contactSalesLink] = useOpenSalesLink();
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<ExternalLink
|
||||
@@ -26,7 +25,7 @@ export default function ContactSalesLink() {
|
||||
'click_contact_sales',
|
||||
);
|
||||
}}
|
||||
href={contactSupportLink}
|
||||
href={contactSalesLink}
|
||||
location='contact_sales_link'
|
||||
>
|
||||
{intl.formatMessage({id: 'self_hosted_signup.contact_sales', defaultMessage: 'Contact Sales'})}
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
import React from 'react';
|
||||
|
||||
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 AccessDeniedHappySvg from 'components/common/svg_images_components/access_denied_happy_svg';
|
||||
import IconMessage from 'components/purchase_modal/icon_message';
|
||||
import {useOpenSelfHostedZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
interface Props {
|
||||
@@ -20,7 +18,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function ErrorPage(props: Props) {
|
||||
const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical);
|
||||
const [, contactSupportLink] = useOpenSelfHostedZendeskSupportForm('Purchase error');
|
||||
let formattedTitle = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.paymentVerificationFailed'
|
||||
|
||||
@@ -310,6 +310,15 @@ describe('SelfHostedPurchaseModal :: canSubmit', () => {
|
||||
state: 'string',
|
||||
country: 'string',
|
||||
postalCode: '12345',
|
||||
|
||||
shippingSame: true,
|
||||
shippingAddress: '',
|
||||
shippingAddress2: '',
|
||||
shippingCity: '',
|
||||
shippingState: '',
|
||||
shippingCountry: '',
|
||||
shippingPostalCode: '',
|
||||
|
||||
cardName: 'string',
|
||||
organization: 'string',
|
||||
agreedTerms: true,
|
||||
@@ -361,6 +370,21 @@ describe('SelfHostedPurchaseModal :: canSubmit', () => {
|
||||
expect(canSubmit(state, SelfHostedSignupProgress.CREATED_CUSTOMER)).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', () => {
|
||||
const state = makeHappyPathState();
|
||||
state.cardFilled = false;
|
||||
|
||||
@@ -26,8 +26,6 @@ import {GlobalState} from 'types/store';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
import {isDevModeEnabled} from 'selectors/general';
|
||||
|
||||
import {COUNTRIES} from 'utils/countries';
|
||||
|
||||
import {
|
||||
ModalIdentifiers,
|
||||
StatTypes,
|
||||
@@ -35,8 +33,6 @@ import {
|
||||
} from 'utils/constants';
|
||||
|
||||
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 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 useControlSelfHostedPurchaseModal from 'components/common/hooks/useControlSelfHostedPurchaseModal';
|
||||
import useFetchStandardAnalytics from 'components/common/hooks/useFetchStandardAnalytics';
|
||||
import ChooseDifferentShipping from 'components/choose_different_shipping';
|
||||
|
||||
import {ValueOf} from '@mattermost/types/utilities';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
@@ -64,6 +61,7 @@ import SuccessPage from './success_page';
|
||||
import SelfHostedCard from './self_hosted_card';
|
||||
import StripeProvider from './stripe_provider';
|
||||
import Terms from './terms';
|
||||
import Address from './address';
|
||||
import useNoEscape from './useNoEscape';
|
||||
|
||||
import {SetPrefix, UnionSetActions} from './types';
|
||||
@@ -73,12 +71,24 @@ import './self_hosted_purchase_modal.scss';
|
||||
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from './constants';
|
||||
|
||||
export interface State {
|
||||
|
||||
// billing address
|
||||
address: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
postalCode: string;
|
||||
|
||||
// shipping address
|
||||
shippingSame: boolean;
|
||||
shippingAddress: string;
|
||||
shippingAddress2: string;
|
||||
shippingCity: string;
|
||||
shippingState: string;
|
||||
shippingCountry: string;
|
||||
shippingPostalCode: string;
|
||||
|
||||
cardName: string;
|
||||
organization: string;
|
||||
agreedTerms: boolean;
|
||||
@@ -113,6 +123,15 @@ export function makeInitialState(): State {
|
||||
state: '',
|
||||
country: '',
|
||||
postalCode: '',
|
||||
|
||||
shippingSame: true,
|
||||
shippingAddress: '',
|
||||
shippingAddress2: '',
|
||||
shippingCity: '',
|
||||
shippingState: '',
|
||||
shippingCountry: '',
|
||||
shippingPostalCode: '',
|
||||
|
||||
cardName: '',
|
||||
organization: '',
|
||||
agreedTerms: false,
|
||||
@@ -170,8 +189,18 @@ const simpleSetters: Array<Extract<keyof State, string>> = [
|
||||
'address2',
|
||||
'city',
|
||||
'country',
|
||||
'postalCode',
|
||||
'state',
|
||||
'postalCode',
|
||||
|
||||
// shipping address
|
||||
'shippingSame',
|
||||
'shippingAddress',
|
||||
'shippingAddress2',
|
||||
'shippingCity',
|
||||
'shippingState',
|
||||
'shippingCountry',
|
||||
'shippingPostalCode',
|
||||
|
||||
'agreedTerms',
|
||||
'cardFilled',
|
||||
'cardName',
|
||||
@@ -220,7 +249,7 @@ export function canSubmit(state: State, progress: ValueOf<typeof SelfHostedSignu
|
||||
return false;
|
||||
}
|
||||
|
||||
const validAddress = Boolean(
|
||||
let validAddress = Boolean(
|
||||
state.organization &&
|
||||
state.address &&
|
||||
state.city &&
|
||||
@@ -228,6 +257,16 @@ export function canSubmit(state: State, progress: ValueOf<typeof SelfHostedSignu
|
||||
state.postalCode &&
|
||||
state.country,
|
||||
);
|
||||
if (!state.shippingSame) {
|
||||
validAddress = validAddress && Boolean(
|
||||
state.shippingAddress &&
|
||||
state.shippingCity &&
|
||||
state.shippingState &&
|
||||
state.shippingPostalCode &&
|
||||
state.shippingCountry,
|
||||
|
||||
);
|
||||
}
|
||||
const validCard = Boolean(
|
||||
state.cardName &&
|
||||
state.cardFilled,
|
||||
@@ -366,16 +405,25 @@ export default function SelfHostedPurchaseModal(props: Props) {
|
||||
try {
|
||||
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({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
billing_address: {
|
||||
city: state.city,
|
||||
country: state.country,
|
||||
line1: state.address,
|
||||
line2: state.address2,
|
||||
postal_code: state.postalCode,
|
||||
state: state.state,
|
||||
billing_address: billingAddress,
|
||||
shipping_address: state.shippingSame ? billingAddress : {
|
||||
city: state.shippingCity,
|
||||
country: state.shippingCountry,
|
||||
line1: state.shippingAddress,
|
||||
line2: state.shippingAddress2,
|
||||
postal_code: state.shippingPostalCode,
|
||||
state: state.shippingState,
|
||||
},
|
||||
organization: state.organization,
|
||||
});
|
||||
@@ -586,99 +634,76 @@ export default function SelfHostedPurchaseModal(props: Props) {
|
||||
defaultMessage='Billing address'
|
||||
/>
|
||||
</div>
|
||||
<DropdownInput
|
||||
testId='selfHostedPurchaseCountrySelector'
|
||||
onChange={(option: {value: string}) => {
|
||||
<Address
|
||||
type='billing'
|
||||
country={state.country}
|
||||
changeCountry={(option) => {
|
||||
dispatch({type: 'set_country', data: option.value});
|
||||
}}
|
||||
value={
|
||||
state.country ? {value: state.country, label: state.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'}
|
||||
address={state.address}
|
||||
changeAddress={(e) => {
|
||||
dispatch({type: 'set_address', data: e.target.value});
|
||||
}}
|
||||
address2={state.address2}
|
||||
changeAddress2={(e) => {
|
||||
dispatch({type: 'set_address2', data: e.target.value});
|
||||
}}
|
||||
city={state.city}
|
||||
changeCity={(e) => {
|
||||
dispatch({type: 'set_city', data: e.target.value});
|
||||
}}
|
||||
state={state.state}
|
||||
changeState={(state: string) => {
|
||||
dispatch({type: 'set_state', data: state});
|
||||
}}
|
||||
postalCode={state.postalCode}
|
||||
changePostalCode={(e) => {
|
||||
dispatch({type: 'set_postalCode', data: e.target.value});
|
||||
}}
|
||||
/>
|
||||
<div className='form-row'>
|
||||
<Input
|
||||
name='address'
|
||||
type='text'
|
||||
value={state.address}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch({type: 'set_address', data: e.target.value});
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'payment_form.address',
|
||||
defaultMessage: 'Address',
|
||||
})}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-row'>
|
||||
<Input
|
||||
name='address2'
|
||||
type='text'
|
||||
value={state.address2}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch({type: 'set_address2', data: e.target.value});
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'payment_form.address_2',
|
||||
defaultMessage: 'Address 2',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-row'>
|
||||
<Input
|
||||
name='city'
|
||||
type='text'
|
||||
value={state.city}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch({type: 'set_city', data: e.target.value});
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'payment_form.city',
|
||||
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});
|
||||
<ChooseDifferentShipping
|
||||
shippingIsSame={state.shippingSame}
|
||||
setShippingIsSame={(val: boolean) => {
|
||||
dispatch({type: 'set_shippingSame', data: val});
|
||||
}}
|
||||
/>
|
||||
{!state.shippingSame && (
|
||||
<>
|
||||
<div className='section-title'>
|
||||
<FormattedMessage
|
||||
id='payment_form.shipping_address'
|
||||
defaultMessage='Shipping Address'
|
||||
/>
|
||||
</div>
|
||||
<Address
|
||||
type='shipping'
|
||||
country={state.shippingCountry}
|
||||
changeCountry={(option) => {
|
||||
dispatch({type: 'set_shippingCountry', data: option.value});
|
||||
}}
|
||||
address={state.shippingAddress}
|
||||
changeAddress={(e) => {
|
||||
dispatch({type: 'set_shippingAddress', data: e.target.value});
|
||||
}}
|
||||
address2={state.shippingAddress2}
|
||||
changeAddress2={(e) => {
|
||||
dispatch({type: 'set_shippingAddress2', data: e.target.value});
|
||||
}}
|
||||
city={state.shippingCity}
|
||||
changeCity={(e) => {
|
||||
dispatch({type: 'set_shippingCity', data: e.target.value});
|
||||
}}
|
||||
state={state.shippingState}
|
||||
changeState={(state: string) => {
|
||||
dispatch({type: 'set_shippingState', data: state});
|
||||
}}
|
||||
postalCode={state.shippingPostalCode}
|
||||
changePostalCode={(e) => {
|
||||
dispatch({type: 'set_shippingPostalCode', data: e.target.value});
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
agreed={state.agreedTerms}
|
||||
setAgreed={(data: boolean) => {
|
||||
|
||||
@@ -4,19 +4,20 @@
|
||||
|
||||
.form-view {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
align-content: top;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 77px 107px;
|
||||
color: var(--center-channel-color);
|
||||
font-family: "Open Sans";
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
@@ -39,14 +40,12 @@
|
||||
margin-right: 16px;
|
||||
|
||||
.DropdownInput {
|
||||
z-index: 99999;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.DropdownInput {
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
height: 36px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@@ -517,6 +516,9 @@
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
flex-shrink: 0;
|
||||
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 {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';
|
||||
|
||||
@@ -28,7 +28,7 @@ function ADLDAPUpsellBanner() {
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const {formatMessage} = useIntl();
|
||||
const openSalesLink = useOpenSalesLink();
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getPrevTrialLicense());
|
||||
@@ -54,14 +54,6 @@ function ADLDAPUpsellBanner() {
|
||||
|
||||
const currentLicenseEndDate = new Date(parseInt(currentLicense?.ExpiresAt, 10));
|
||||
|
||||
const openLink = () => {
|
||||
if (isCloud) {
|
||||
openSalesLink();
|
||||
} else {
|
||||
window.open(LicenseLinks.CONTACT_SALES, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBanner = (
|
||||
<div className='ad_ldap_upsell_confirm'>
|
||||
<div className='upsell-confirm-backdrop'/>
|
||||
@@ -71,7 +63,7 @@ function ADLDAPUpsellBanner() {
|
||||
<div className='btns-container'>
|
||||
<button
|
||||
className='confrim-btn learn-more'
|
||||
onClick={openLink}
|
||||
onClick={openSalesLink}
|
||||
>
|
||||
{formatMessage({id: 'adldap_upsell_banner.confirm.learn_more', defaultMessage: 'Learn more'})}
|
||||
</button>
|
||||
@@ -122,7 +114,7 @@ function ADLDAPUpsellBanner() {
|
||||
btn = (
|
||||
<button
|
||||
className='ad-ldap-banner-btn'
|
||||
onClick={openLink}
|
||||
onClick={openSalesLink}
|
||||
>
|
||||
{formatMessage({id: 'adldap_upsell_banner.sales_btn', defaultMessage: 'Contact sales to use'})}
|
||||
</button>
|
||||
|
||||
@@ -3484,8 +3484,6 @@
|
||||
"modal.manual_status.title_offline": "Вашето състояние е зададено на \"Извън линия\"",
|
||||
"modal.manual_status.title_ooo": "Вашето състояние е зададено на \"Извън офиса\"",
|
||||
"more_channels.create": "Създайте канал",
|
||||
"more_channels.createClick": "Кликнете върху \"Създаване на нов канал\", за да създадете канал",
|
||||
"more_channels.joining": "Присъединявне ...",
|
||||
"more_channels.next": "Следващ",
|
||||
"more_channels.noMore": "Няма повече канали за присъединяване",
|
||||
"more_channels.prev": "Предишен",
|
||||
|
||||
@@ -2127,7 +2127,7 @@
|
||||
"admin.service.corsExposedHeadersTitle": "CORS-Exposed-Headers:",
|
||||
"admin.service.corsHeadersEx": "X-My-Header",
|
||||
"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.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).",
|
||||
@@ -3603,7 +3603,7 @@
|
||||
"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.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.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:",
|
||||
@@ -4156,13 +4156,22 @@
|
||||
"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",
|
||||
"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.createClick": "Klicke auf 'Neuen Kanal erstellen' um einen Neuen zu erzeugen",
|
||||
"more_channels.join": "Beitreten",
|
||||
"more_channels.joining": "Betrete...",
|
||||
"more_channels.hide_joined": "Verbundene Kanäle ausblenden",
|
||||
"more_channels.hide_joined_checked": "Kontrollkästchen Verbundene Kanäle ausblenden, aktiviert",
|
||||
"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.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.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_public_channels": "Anzeigen: Öffentliche Kanäle",
|
||||
"more_channels.title": "Weitere Kanäle",
|
||||
@@ -4372,6 +4381,7 @@
|
||||
"payment_form.no_billing_address": "Keine Rechnungsadresse hinzugefügt",
|
||||
"payment_form.no_credit_card": "Keine Kreditkarte hinzugefügt",
|
||||
"payment_form.saved_payment_method": "Zahlungsmethode speichern",
|
||||
"payment_form.shipping_address": "Lieferadresse",
|
||||
"payment_form.zipcode": "Postleitzahl/Zip",
|
||||
"pending_post_actions.cancel": "Abbrechen",
|
||||
"pending_post_actions.retry": "Erneut versuchen",
|
||||
|
||||
@@ -4378,6 +4378,7 @@
|
||||
"payment_form.no_billing_address": "No billing address added",
|
||||
"payment_form.no_credit_card": "No credit card added",
|
||||
"payment_form.saved_payment_method": "Saved Payment Method",
|
||||
"payment_form.shipping_address": "Shipping Address",
|
||||
"payment_form.zipcode": "Zip/Postal Code",
|
||||
"payment.card_number": "Card Number",
|
||||
"payment.field_required": "This field is required",
|
||||
|
||||
@@ -4153,9 +4153,6 @@
|
||||
"modal.manual_status.title_ooo": "Your Status is Set to 'Out of Office'",
|
||||
"more.details": "More details",
|
||||
"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.noMore": "No more channels to join",
|
||||
"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.save": "Guardar información",
|
||||
"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.date": "Fecha",
|
||||
"admin.billing.history.description": "Descripción",
|
||||
@@ -3898,9 +3899,6 @@
|
||||
"modal.manual_status.title_ooo": "Tu estado actual es \"Fuera de Oficina\"",
|
||||
"more.details": "Más detalles",
|
||||
"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.noMore": "No hay más canales para unirse",
|
||||
"more_channels.prev": "Anterior",
|
||||
|
||||
@@ -3707,9 +3707,6 @@
|
||||
"modal.manual_status.title_ooo": "وضعیت شما روی \"خارج از دفتر\" تنظیم شده است",
|
||||
"more.details": "جزئیات بیشتر",
|
||||
"more_channels.create": "ایجاد کانال",
|
||||
"more_channels.createClick": "برای ایجاد کانال جدید روی \"ایجاد کانال جدید\" کلیک کنید",
|
||||
"more_channels.join": "پیوستن",
|
||||
"more_channels.joining": "پیوستن...",
|
||||
"more_channels.next": "بعد",
|
||||
"more_channels.noMore": "کانال دیگری برای پیوستن وجود ندارد",
|
||||
"more_channels.prev": "قبلی",
|
||||
|
||||
@@ -3736,9 +3736,6 @@
|
||||
"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 »",
|
||||
"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.noMore": "Il n'y a plus d'autre canal que vous pouvez rejoindre",
|
||||
"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",
|
||||
"more.details": "További információ",
|
||||
"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.noMore": "Nincs több beszélgetés amelyhez csatlakozni lehetne",
|
||||
"more_channels.prev": "Előző",
|
||||
|
||||
@@ -2997,8 +2997,6 @@
|
||||
"modal.manual_status.title_offline": "Il tuo stato è \"Non in linea\"",
|
||||
"modal.manual_status.title_ooo": "Il tuo stato è \"Fuori sede\"",
|
||||
"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.noMore": "Nessun altro canale in cui entrare",
|
||||
"more_channels.prev": "Precedente",
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
"admin.billing.subscription.privateCloudCard.contactSalesy": "営業担当に問い合わせる",
|
||||
"admin.billing.subscription.privateCloudCard.contactSupport": "サポートに連絡する",
|
||||
"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.proratedPayment.substitle": "{selectedProductName}にアップグレードしていただきありがとうございます。このプランのすべての機能にアクセスするには、数分後にワークスペースをチェックしてください。現在ご利用中の{currentProductName}プランと{selectedProductName}プランの料金については、請求サイクルの残り日数とユーザー数に応じた額を請求させていただきます。",
|
||||
"admin.billing.subscription.proratedPayment.title": "現在、{selectedProductName} を利用しています",
|
||||
@@ -1989,6 +1989,8 @@
|
||||
"admin.requestButton.requestFailure": "テストが失敗しました: {error}",
|
||||
"admin.requestButton.requestSuccess": "テストが成功しました",
|
||||
"admin.reset_email.cancel": "キャンセル",
|
||||
"admin.reset_email.currentPassword": "現在のパスワード",
|
||||
"admin.reset_email.missing_current_password": "現在のパスワードを入力してください。",
|
||||
"admin.reset_email.newEmail": "新しい電子メールアドレス",
|
||||
"admin.reset_email.reset": "リセット",
|
||||
"admin.reset_email.titleReset": "電子メールを更新する",
|
||||
@@ -2125,7 +2127,7 @@
|
||||
"admin.service.corsExposedHeadersTitle": "CORS Exposedヘッダ:",
|
||||
"admin.service.corsHeadersEx": "X-My-Header",
|
||||
"admin.service.corsTitle": "クロスオリジンリクエストを許可する:",
|
||||
"admin.service.developerDesc": "有効な場合、JavaScriptのエラーはユーザーインターフェイス上部の紫色のバーに表示されます。本番環境での使用はお勧めできません。 ",
|
||||
"admin.service.developerDesc": "有効な場合、JavaScriptのエラーはユーザーインターフェイス上部の紫色のバーに表示されます。本番環境での使用はお勧めできません。",
|
||||
"admin.service.developerTitle": "開発者モードを有効にする: ",
|
||||
"admin.service.disableBotOwnerDeactivatedTitle": "オーナーが無効化された際にBotアカウントを無効化する:",
|
||||
"admin.service.disableBotWhenOwnerIsDeactivated": "ユーザーが無効化された際、そのユーザーが管理していたすべてのBotアカウントを無効化します。Botアカウントを再び有効にするには、[統合機能 > Botアカウント]({siteURL}/_redirect/integrations/bots) から設定してください。",
|
||||
@@ -3128,7 +3130,7 @@
|
||||
"create_post.deactivated": "**無効化されたユーザー** のいるアーカイブされたチャンネルを見ています。新しいメッセージは投稿できません。",
|
||||
"create_post.error_message": "メッセージが長すぎます。文字数: {length}/{limit}",
|
||||
"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.non_admin_message": "新たにアップロードすると古いファイルから自動的にアーカイブされます。再度表示するには、<a>管理者に有料プランへアップグレードするよう通知してください</a>。",
|
||||
"create_post.file_limit_sticky_banner.snooze_tooltip": "{snoozeDays}日間スヌーズする",
|
||||
@@ -4154,13 +4156,22 @@
|
||||
"modal.manual_status.title_offline": "ステータスが \"オフライン\" になりました",
|
||||
"modal.manual_status.title_ooo": "ステータスが \"外出中\" になりました",
|
||||
"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.createClick": "新しいチャンネルを作成するには「チャンネルを作成する」をクリックしてください",
|
||||
"more_channels.join": "参加",
|
||||
"more_channels.joining": "参加しています....",
|
||||
"more_channels.hide_joined": "参加したことを表示しない",
|
||||
"more_channels.hide_joined_checked": "チャンネルに参加したことを表示しないチェックボックスがチェック済です",
|
||||
"more_channels.hide_joined_not_checked": "チャンネルに参加したことを表示しないチェックボックスがチェックされていません",
|
||||
"more_channels.joined": "参加済",
|
||||
"more_channels.membership_indicator": "メンバーシップ状況: 参加済",
|
||||
"more_channels.next": "次へ",
|
||||
"more_channels.noMore": "参加できるチャンネルがありません",
|
||||
"more_channels.noArchived": "アーカイブされたチャンネルはありません",
|
||||
"more_channels.noMore": "\"{text}\"の結果はありません",
|
||||
"more_channels.noPublic": "公開チャンネルはありません",
|
||||
"more_channels.prev": "前へ",
|
||||
"more_channels.searchError": "違うキーワードで検索してみたり、入力ミスを確認したり、フィルター設定を変更して再度お試しください。",
|
||||
"more_channels.show_archived_channels": "表示: アーカイブチャンネル",
|
||||
"more_channels.show_public_channels": "表示: 公開チャンネル",
|
||||
"more_channels.title": "他のチャンネル",
|
||||
@@ -4370,6 +4381,7 @@
|
||||
"payment_form.no_billing_address": "請求先住所が追加されませんでした",
|
||||
"payment_form.no_credit_card": "クレジットカードが追加されませんでした",
|
||||
"payment_form.saved_payment_method": "支払い方法を保存する",
|
||||
"payment_form.shipping_address": "配送先住所",
|
||||
"payment_form.zipcode": "郵便番号",
|
||||
"pending_post_actions.cancel": "キャンセル",
|
||||
"pending_post_actions.retry": "再試行",
|
||||
@@ -4385,6 +4397,14 @@
|
||||
"plan.self_serve": "セルフサービス",
|
||||
"pluggable.errorOccurred": "プラグイン {pluginId} でエラーが発生しました。",
|
||||
"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.attachmentMultiple": ", {attachmentCount} 添付ファイル",
|
||||
"post.ariaLabel.message": "{time} {date}, {authorName} が, {message} を書きました",
|
||||
@@ -4394,6 +4414,8 @@
|
||||
"post.ariaLabel.reaction": ", 1 リアクション",
|
||||
"post.ariaLabel.reactionMultiple": ", {reactionCount} リアクション",
|
||||
"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_mentions.link.and": " と ",
|
||||
"post_body.check_for_out_of_channel_mentions.link.private": "彼らを非公開チャンネルに追加しますか",
|
||||
@@ -4434,6 +4456,13 @@
|
||||
"post_info.message.visible.compact": " (あなただけが見ることができます)",
|
||||
"post_info.permalink": "リンクをコピーする",
|
||||
"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.submenu.icon": "サブメニューアイコン",
|
||||
"post_info.submenu.mobile": "モバイルサブメニュー",
|
||||
@@ -4462,6 +4491,9 @@
|
||||
"post_priority.requested_ack.description": "メッセージに確認ボタンが表示されます",
|
||||
"post_priority.requested_ack.text": "確認を要求する",
|
||||
"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.newMessages": "新しい {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_long": "電子メール通知はシステム管理者によって有効化されていません。",
|
||||
"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.never": "通知しない",
|
||||
"user.settings.notifications.email.send": "電子メール通知を送信する",
|
||||
|
||||
@@ -2883,8 +2883,6 @@
|
||||
"modal.manual_status.title_offline": "상태가 \"오프라인\"이 되셨습니다",
|
||||
"modal.manual_status.title_ooo": "상태가 \"오프라인\"이 되셨습니다",
|
||||
"more_channels.create": "채널 만들기",
|
||||
"more_channels.createClick": "'새로 만들기'를 클릭하여 새로운 채널을 만드세요",
|
||||
"more_channels.joining": "참가 중...",
|
||||
"more_channels.next": "다음",
|
||||
"more_channels.noMore": "가입할 수 있는 채널이 없습니다",
|
||||
"more_channels.prev": "이전",
|
||||
|
||||
@@ -4155,9 +4155,6 @@
|
||||
"modal.manual_status.title_ooo": "Je status is ingesteld op \"Out of Office\"",
|
||||
"more.details": "Meer details",
|
||||
"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.noMore": "Geen kanalen beschikbaar waar aan deelgenomen kan worden",
|
||||
"more_channels.prev": "Vorige",
|
||||
|
||||
@@ -2127,7 +2127,7 @@
|
||||
"admin.service.corsExposedHeadersTitle": "Eksponowane nagłówki CORS:",
|
||||
"admin.service.corsHeadersEx": "X-Mój-Header",
|
||||
"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.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).",
|
||||
@@ -4156,13 +4156,22 @@
|
||||
"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\"",
|
||||
"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.createClick": "Kliknij przycisk 'Utwórz nowy kanał', aby dodać nowy",
|
||||
"more_channels.join": "Dołącz do",
|
||||
"more_channels.joining": "Dołączanie...",
|
||||
"more_channels.hide_joined": "Ukryj dołączonych",
|
||||
"more_channels.hide_joined_checked": "Pole wyboru Ukryj połączone kanały, zaznaczone",
|
||||
"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.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.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_public_channels": "Pokaż: Publiczne kanały",
|
||||
"more_channels.title": "Więcej Kanałów",
|
||||
@@ -4372,6 +4381,7 @@
|
||||
"payment_form.no_billing_address": "Nie dodano adresu rozliczeniowego",
|
||||
"payment_form.no_credit_card": "Nie dodano karty kredytowej",
|
||||
"payment_form.saved_payment_method": "Zapisana Metoda Płatności",
|
||||
"payment_form.shipping_address": "Adres do wysyłki",
|
||||
"payment_form.zipcode": "Kod Pocztowy",
|
||||
"pending_post_actions.cancel": "Anuluj",
|
||||
"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_ooo": "Seu Status está configurado para \"Fora do Escritório\"",
|
||||
"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.noMore": "Não há mais canais para participar",
|
||||
"more_channels.prev": "Anterior",
|
||||
|
||||
@@ -3306,8 +3306,6 @@
|
||||
"modal.manual_status.title_offline": "Starea dvs. este setată la \"Offline\"",
|
||||
"modal.manual_status.title_ooo": "Starea dvs. este setată la \"Plecat din birou\"",
|
||||
"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.noMore": "Nu mai există canale care să se alăture",
|
||||
"more_channels.prev": "Anterior",
|
||||
|
||||
@@ -4157,9 +4157,6 @@
|
||||
"modal.manual_status.title_ooo": "Ваш статус установлен на \"Не на работе\"",
|
||||
"more.details": "Подробнее",
|
||||
"more_channels.create": "Создать канал",
|
||||
"more_channels.createClick": "Нажмите 'Создать канал' для создания нового канала",
|
||||
"more_channels.join": "Присоединиться",
|
||||
"more_channels.joining": "Присоединяемся...",
|
||||
"more_channels.next": "Далее",
|
||||
"more_channels.noMore": "Доступных каналов не найдено",
|
||||
"more_channels.prev": "Предыдущая",
|
||||
|
||||
@@ -4157,9 +4157,6 @@
|
||||
"modal.manual_status.title_ooo": "Din status är satt till \"Inte på kontoret\"",
|
||||
"more.details": "Mer information",
|
||||
"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.noMore": "Det finns inga fler kanaler att gå med i",
|
||||
"more_channels.prev": "Föregående",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user