Revert "MM-49603 : Remove fetching of deleted channels on page load (#22981)"
This reverts commit 1bbfdefad7.
Этот коммит содержится в:
10
.github/workflows/channels-ci.yml
поставляемый
10
.github/workflows/channels-ci.yml
поставляемый
@@ -83,16 +83,6 @@ jobs:
|
|||||||
npm run mmjstool -- i18n clean-empty --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir --check
|
npm run mmjstool -- i18n clean-empty --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir --check
|
||||||
npm run mmjstool -- i18n check-empty-src --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir
|
npm run mmjstool -- i18n check-empty-src --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir
|
||||||
rm -rf tmp
|
rm -rf tmp
|
||||||
- name: ci/lint-boards
|
|
||||||
working-directory: webapp/boards
|
|
||||||
run: |
|
|
||||||
npm run i18n-extract
|
|
||||||
git --no-pager diff --exit-code i18n/en.json || (echo "Please run \"cd webapp/boards && npm run i18n-extract\" and commit the changes in webapp/boards/i18n/en.json." && exit 1)
|
|
||||||
- name: ci/lint-playbooks
|
|
||||||
working-directory: webapp/playbooks
|
|
||||||
run: |
|
|
||||||
npm run i18n-extract
|
|
||||||
git --no-pager diff --exit-code i18n/en.json || (echo "Please run \"cd webapp/playbooks && npm run i18n-extract\" and commit the changes in webapp/playbooks/i18n/en.json." && exit 1)
|
|
||||||
check-types:
|
check-types:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
defaults:
|
defaults:
|
||||||
|
|||||||
@@ -5,6 +5,3 @@
|
|||||||
/webapp/package-lock.json @mattermost/web-platform
|
/webapp/package-lock.json @mattermost/web-platform
|
||||||
/webapp/platform/*/package.json @mattermost/web-platform
|
/webapp/platform/*/package.json @mattermost/web-platform
|
||||||
/webapp/scripts @mattermost/web-platform
|
/webapp/scripts @mattermost/web-platform
|
||||||
/server/channels/db/migrations @mattermost/server-platform
|
|
||||||
/server/boards/services/store/sqlstore/migrations @mattermost/server-platform
|
|
||||||
/server/playbooks/server/sqlstore/migrations @mattermost/server-platform
|
|
||||||
|
|||||||
@@ -665,6 +665,7 @@ const defaultServerConfig: AdminConfig = {
|
|||||||
BoardsFeatureFlags: '',
|
BoardsFeatureFlags: '',
|
||||||
BoardsDataRetention: false,
|
BoardsDataRetention: false,
|
||||||
NormalizeLdapDNs: false,
|
NormalizeLdapDNs: false,
|
||||||
|
UseCaseOnboarding: true,
|
||||||
GraphQL: false,
|
GraphQL: false,
|
||||||
InsightsEnabled: true,
|
InsightsEnabled: true,
|
||||||
CommandPalette: false,
|
CommandPalette: false,
|
||||||
|
|||||||
@@ -70,10 +70,7 @@ func (s *SQLStore) getMigrationConnection() (*sql.DB, error) {
|
|||||||
}
|
}
|
||||||
*settings.DriverName = s.dbType
|
*settings.DriverName = s.dbType
|
||||||
|
|
||||||
db, err := sqlstore.SetupConnection("master", connectionString, &settings, sqlstore.DBPingAttempts)
|
db := sqlstore.SetupConnection("master", connectionString, &settings)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -892,7 +892,6 @@ func TestCompleteOnboarding(t *testing.T) {
|
|||||||
|
|
||||||
req := &model.CompleteOnboardingRequest{
|
req := &model.CompleteOnboardingRequest{
|
||||||
InstallPlugins: []string{"testplugin2"},
|
InstallPlugins: []string{"testplugin2"},
|
||||||
Organization: "my-org",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("as a regular user", func(t *testing.T) {
|
t.Run("as a regular user", func(t *testing.T) {
|
||||||
|
|||||||
@@ -3106,10 +3106,6 @@ func getThreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
|
||||||
c.SetPermissionError(model.PermissionReadChannel)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
extendedStr := r.URL.Query().Get("extended")
|
extendedStr := r.URL.Query().Get("extended")
|
||||||
extended, _ := strconv.ParseBool(extendedStr)
|
extended, _ := strconv.ParseBool(extendedStr)
|
||||||
|
|
||||||
@@ -3140,10 +3136,6 @@ func getThreadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
|
||||||
c.SetPermissionError(model.PermissionViewTeam)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options := model.GetUserThreadsOpts{
|
options := model.GetUserThreadsOpts{
|
||||||
Since: 0,
|
Since: 0,
|
||||||
@@ -3221,10 +3213,6 @@ func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Requ
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
|
||||||
c.SetPermissionError(model.PermissionReadChannel)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp)
|
thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3291,10 +3279,6 @@ func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
|
||||||
c.SetPermissionError(model.PermissionReadChannel)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false)
|
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3354,10 +3338,6 @@ func updateReadStateAllThreadsByUser(c *Context, w http.ResponseWriter, r *http.
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
|
||||||
c.SetPermissionError(model.PermissionViewTeam)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId)
|
err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6360,15 +6360,6 @@ func TestGetThreadsForUser(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, uss.TotalUnreadThreads, int64(2))
|
require.Equal(t, uss.TotalUnreadThreads, int64(2))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should error when not a team member", func(t *testing.T) {
|
|
||||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
|
||||||
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
|
||||||
|
|
||||||
_, resp, err := th.Client.GetUserThreads(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
|
|
||||||
require.Error(t, err)
|
|
||||||
CheckForbiddenStatus(t, resp)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestThreadSocketEvents(t *testing.T) {
|
func TestThreadSocketEvents(t *testing.T) {
|
||||||
@@ -6864,8 +6855,6 @@ func TestSingleThreadGet(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
client := th.Client
|
client := th.Client
|
||||||
|
|
||||||
t.Run("get single thread", func(t *testing.T) {
|
|
||||||
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
|
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
|
||||||
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.SystemAdminUser.Id)
|
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.SystemAdminUser.Id)
|
||||||
|
|
||||||
@@ -6912,16 +6901,6 @@ func TestSingleThreadGet(t *testing.T) {
|
|||||||
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, true, tr.IsUrgent)
|
require.Equal(t, true, tr.IsUrgent)
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("should error when not a team member", func(t *testing.T) {
|
|
||||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
|
||||||
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
|
||||||
|
|
||||||
_, resp, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), false)
|
|
||||||
require.Error(t, err)
|
|
||||||
CheckForbiddenStatus(t, resp)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMaintainUnreadMentionsInThread(t *testing.T) {
|
func TestMaintainUnreadMentionsInThread(t *testing.T) {
|
||||||
@@ -7093,23 +7072,6 @@ func TestReadThreads(t *testing.T) {
|
|||||||
|
|
||||||
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil)
|
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should error when not a team member", func(t *testing.T) {
|
|
||||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
|
||||||
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
|
||||||
|
|
||||||
_, resp, err := th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.GetMillis())
|
|
||||||
require.Error(t, err)
|
|
||||||
CheckForbiddenStatus(t, resp)
|
|
||||||
|
|
||||||
_, resp, err = th.Client.SetThreadUnreadByPostId(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.NewId())
|
|
||||||
require.Error(t, err)
|
|
||||||
CheckForbiddenStatus(t, resp)
|
|
||||||
|
|
||||||
resp, err = th.Client.UpdateThreadsReadForUser(th.BasicUser.Id, th.BasicTeam.Id)
|
|
||||||
require.Error(t, err)
|
|
||||||
CheckForbiddenStatus(t, resp)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarkThreadUnreadMentionCount(t *testing.T) {
|
func TestMarkThreadUnreadMentionCount(t *testing.T) {
|
||||||
|
|||||||
@@ -2518,9 +2518,6 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove
|
|||||||
if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil {
|
if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil {
|
||||||
return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
if err := a.Srv().Store().Thread().DeleteMembershipsForChannel(userIDToRemove, channel.Id); err != nil {
|
|
||||||
return model.NewAppError("removeUserFromChannel", model.NoTranslation, nil, "failed to delete threadmemberships upon leaving channel", http.StatusInternalServerError).Wrap(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isGuest {
|
if isGuest {
|
||||||
currentMembers, err := a.GetChannelMembersForUser(c, channel.TeamId, userIDToRemove)
|
currentMembers, err := a.GetChannelMembersForUser(c, channel.TeamId, userIDToRemove)
|
||||||
|
|||||||
@@ -609,85 +609,6 @@ func TestLeaveDefaultChannel(t *testing.T) {
|
|||||||
_, err = th.App.GetChannelMember(th.Context, townSquare.Id, guest.Id)
|
_, err = th.App.GetChannelMember(th.Context, townSquare.Id, guest.Id)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Trying to leave the default channel should not delete thread memberships", func(t *testing.T) {
|
|
||||||
post := &model.Post{
|
|
||||||
ChannelId: townSquare.Id,
|
|
||||||
Message: "root post",
|
|
||||||
UserId: th.BasicUser.Id,
|
|
||||||
}
|
|
||||||
rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
reply := &model.Post{
|
|
||||||
ChannelId: townSquare.Id,
|
|
||||||
Message: "reply post",
|
|
||||||
UserId: th.BasicUser.Id,
|
|
||||||
RootId: rpost.Id,
|
|
||||||
}
|
|
||||||
_, err = th.App.CreatePost(th.Context, reply, th.BasicChannel, false, true)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
threads, err := th.App.GetThreadsForUser(th.BasicUser.Id, townSquare.TeamId, model.GetUserThreadsOpts{})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Len(t, threads.Threads, 1)
|
|
||||||
|
|
||||||
err = th.App.LeaveChannel(th.Context, townSquare.Id, th.BasicUser.Id)
|
|
||||||
assert.NotNil(t, err, "It should fail to remove a regular user from the default channel")
|
|
||||||
assert.Equal(t, err.Id, "api.channel.remove.default.app_error")
|
|
||||||
|
|
||||||
threads, err = th.App.GetThreadsForUser(th.BasicUser.Id, townSquare.TeamId, model.GetUserThreadsOpts{})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Len(t, threads.Threads, 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLeaveChannel(t *testing.T) {
|
|
||||||
th := Setup(t).InitBasic()
|
|
||||||
defer th.TearDown()
|
|
||||||
|
|
||||||
createThread := func(channel *model.Channel) (rpost *model.Post) {
|
|
||||||
t.Helper()
|
|
||||||
post := &model.Post{
|
|
||||||
ChannelId: channel.Id,
|
|
||||||
Message: "root post",
|
|
||||||
UserId: th.BasicUser.Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
reply := &model.Post{
|
|
||||||
ChannelId: channel.Id,
|
|
||||||
Message: "reply post",
|
|
||||||
UserId: th.BasicUser.Id,
|
|
||||||
RootId: rpost.Id,
|
|
||||||
}
|
|
||||||
_, err = th.App.CreatePost(th.Context, reply, th.BasicChannel, false, true)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
return rpost
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("thread memberships are deleted", func(t *testing.T) {
|
|
||||||
createThread(th.BasicChannel)
|
|
||||||
channel2 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
|
|
||||||
createThread(channel2)
|
|
||||||
|
|
||||||
threads, err := th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicChannel.TeamId, model.GetUserThreadsOpts{})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Len(t, threads.Threads, 2)
|
|
||||||
|
|
||||||
err = th.App.LeaveChannel(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
_, err = th.App.GetChannelMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
||||||
require.NotNil(t, err, "It should remove channel membership")
|
|
||||||
|
|
||||||
threads, err = th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicChannel.TeamId, model.GetUserThreadsOpts{})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Len(t, threads.Threads, 1)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLeaveLastChannel(t *testing.T) {
|
func TestLeaveLastChannel(t *testing.T) {
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ func TestGetSanitizedClientLicense(t *testing.T) {
|
|||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
_, ok = m["SkuName"]
|
_, ok = m["SkuName"]
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
_, ok = m["SkuShortName"]
|
||||||
|
assert.False(t, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateRenewalToken(t *testing.T) {
|
func TestGenerateRenewalToken(t *testing.T) {
|
||||||
|
|||||||
@@ -28,24 +28,6 @@ func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
|
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
|
||||||
isCloud := a.Srv().License() != nil && *a.Srv().License().Features.Cloud
|
|
||||||
|
|
||||||
if !isCloud && request.Organization == "" {
|
|
||||||
mlog.Error("No organization name provided for self hosted onboarding")
|
|
||||||
return model.NewAppError("CompleteOnboarding", "api.error_no_organization_name_provided_for_self_hosted_onboarding", nil, "", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Organization != "" {
|
|
||||||
err := a.Srv().Store().System().SaveOrUpdate(&model.System{
|
|
||||||
Name: model.SystemOrganizationName,
|
|
||||||
Value: request.Organization,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// don't block onboarding because of that.
|
|
||||||
a.Log().Error("failed to save organization name", mlog.Err(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
|
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
|
||||||
if pluginsEnvironment == nil {
|
if pluginsEnvironment == nil {
|
||||||
return a.markAdminOnboardingComplete(c)
|
return a.markAdminOnboardingComplete(c)
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
|
||||||
mm_model "github.com/mattermost/mattermost-server/server/v8/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOnboardingSavesOrganizationName(t *testing.T) {
|
|
||||||
th := Setup(t)
|
|
||||||
defer th.TearDown()
|
|
||||||
|
|
||||||
err := th.App.CompleteOnboarding(&request.Context{}, &mm_model.CompleteOnboardingRequest{
|
|
||||||
Organization: "Mattermost In Tests",
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer func() {
|
|
||||||
th.App.Srv().Store().System().PermanentDeleteByName(mm_model.SystemOrganizationName)
|
|
||||||
}()
|
|
||||||
|
|
||||||
sys, storeErr := th.App.Srv().Store().System().GetByName(mm_model.SystemOrganizationName)
|
|
||||||
require.NoError(t, storeErr)
|
|
||||||
require.Equal(t, "Mattermost In Tests", sys.Value)
|
|
||||||
}
|
|
||||||
@@ -71,6 +71,8 @@ func TestGetSanitizedClientLicense(t *testing.T) {
|
|||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
_, ok = m["SkuName"]
|
_, ok = m["SkuName"]
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
_, ok = m["SkuShortName"]
|
||||||
|
assert.False(t, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateRenewalToken(t *testing.T) {
|
func TestGenerateRenewalToken(t *testing.T) {
|
||||||
|
|||||||
@@ -212,8 +212,6 @@ channels/db/migrations/mysql/000105_remove_tokens.down.sql
|
|||||||
channels/db/migrations/mysql/000105_remove_tokens.up.sql
|
channels/db/migrations/mysql/000105_remove_tokens.up.sql
|
||||||
channels/db/migrations/mysql/000106_fileinfo_channelid.down.sql
|
channels/db/migrations/mysql/000106_fileinfo_channelid.down.sql
|
||||||
channels/db/migrations/mysql/000106_fileinfo_channelid.up.sql
|
channels/db/migrations/mysql/000106_fileinfo_channelid.up.sql
|
||||||
channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql
|
|
||||||
channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql
|
|
||||||
channels/db/migrations/postgres/000001_create_teams.down.sql
|
channels/db/migrations/postgres/000001_create_teams.down.sql
|
||||||
channels/db/migrations/postgres/000001_create_teams.up.sql
|
channels/db/migrations/postgres/000001_create_teams.up.sql
|
||||||
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
||||||
@@ -426,5 +424,3 @@ channels/db/migrations/postgres/000105_remove_tokens.down.sql
|
|||||||
channels/db/migrations/postgres/000105_remove_tokens.up.sql
|
channels/db/migrations/postgres/000105_remove_tokens.up.sql
|
||||||
channels/db/migrations/postgres/000106_fileinfo_channelid.down.sql
|
channels/db/migrations/postgres/000106_fileinfo_channelid.down.sql
|
||||||
channels/db/migrations/postgres/000106_fileinfo_channelid.up.sql
|
channels/db/migrations/postgres/000106_fileinfo_channelid.up.sql
|
||||||
channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql
|
|
||||||
channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
-- Skipping it because the forward migrations are destructive
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
DELETE FROM
|
|
||||||
tm USING ThreadMemberships AS tm
|
|
||||||
JOIN Threads ON Threads.PostId = tm.PostId
|
|
||||||
WHERE
|
|
||||||
(tm.UserId, Threads.ChannelId) NOT IN (SELECT UserId, ChannelId FROM ChannelMembers);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- Skipping it because the forward migrations are destructive
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
DELETE FROM threadmemberships WHERE (postid, userid) IN (
|
|
||||||
SELECT
|
|
||||||
threadmemberships.postid,
|
|
||||||
threadmemberships.userid
|
|
||||||
FROM
|
|
||||||
threadmemberships
|
|
||||||
JOIN threads ON threads.postid = threadmemberships.postid
|
|
||||||
LEFT JOIN channelmembers ON channelmembers.userid = threadmemberships.userid
|
|
||||||
AND threads.channelid = channelmembers.channelid
|
|
||||||
WHERE
|
|
||||||
channelmembers.channelid IS NULL
|
|
||||||
);
|
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
type MetricsInterface interface {
|
type MetricsInterface interface {
|
||||||
Register()
|
Register()
|
||||||
RegisterDBCollector(db *sql.DB, name string)
|
RegisterDBCollector(db *sql.DB, name string)
|
||||||
UnregisterDBCollector(db *sql.DB, name string)
|
|
||||||
|
|
||||||
IncrementPostCreate()
|
IncrementPostCreate()
|
||||||
IncrementWebhookPost()
|
IncrementWebhookPost()
|
||||||
|
|||||||
@@ -319,11 +319,6 @@ func (_m *MetricsInterface) SetReplicaLagTime(node string, value float64) {
|
|||||||
_m.Called(node, value)
|
_m.Called(node, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnregisterDBCollector provides a mock function with given fields: db, name
|
|
||||||
func (_m *MetricsInterface) UnregisterDBCollector(db *sql.DB, name string) {
|
|
||||||
_m.Called(db, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockConstructorTestingTNewMetricsInterface interface {
|
type mockConstructorTestingTNewMetricsInterface interface {
|
||||||
mock.TestingT
|
mock.TestingT
|
||||||
Cleanup(func())
|
Cleanup(func())
|
||||||
|
|||||||
@@ -10123,24 +10123,6 @@ func (s *OpenTracingLayerThreadStore) DeleteMembershipForUser(userId string, pos
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OpenTracingLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
|
||||||
origCtx := s.Root.Store.Context()
|
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteMembershipsForChannel")
|
|
||||||
s.Root.Store.SetContext(newCtx)
|
|
||||||
defer func() {
|
|
||||||
s.Root.Store.SetContext(origCtx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
defer span.Finish()
|
|
||||||
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
|
||||||
if err != nil {
|
|
||||||
span.LogFields(spanlog.Error(err))
|
|
||||||
ext.Error.Set(span, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OpenTracingLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
func (s *OpenTracingLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||||
origCtx := s.Root.Store.Context()
|
origCtx := s.Root.Store.Context()
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteOrphanedRows")
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteOrphanedRows")
|
||||||
|
|||||||
@@ -11563,27 +11563,6 @@ func (s *RetryLayerThreadStore) DeleteMembershipForUser(userId string, postID st
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RetryLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
|
||||||
|
|
||||||
tries := 0
|
|
||||||
for {
|
|
||||||
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !isRepeatableError(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tries++
|
|
||||||
if tries >= 3 {
|
|
||||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RetryLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
func (s *RetryLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor
|
|||||||
Id: newCategoryId,
|
Id: newCategoryId,
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
TeamId: teamId,
|
TeamId: teamId,
|
||||||
Sorting: newCategory.Sorting,
|
Sorting: model.SidebarCategorySortDefault,
|
||||||
SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list
|
SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list
|
||||||
Type: model.SidebarCategoryCustom,
|
Type: model.SidebarCategoryCustom,
|
||||||
Muted: newCategory.Muted,
|
Muted: newCategory.Muted,
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ package sqlstore
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -69,18 +66,14 @@ type sqlxDBWrapper struct {
|
|||||||
*sqlx.DB
|
*sqlx.DB
|
||||||
queryTimeout time.Duration
|
queryTimeout time.Duration
|
||||||
trace bool
|
trace bool
|
||||||
isOnline *atomic.Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper {
|
func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper {
|
||||||
w := &sqlxDBWrapper{
|
return &sqlxDBWrapper{
|
||||||
DB: db,
|
DB: db,
|
||||||
queryTimeout: timeout,
|
queryTimeout: timeout,
|
||||||
trace: trace,
|
trace: trace,
|
||||||
isOnline: &atomic.Bool{},
|
|
||||||
}
|
}
|
||||||
w.isOnline.Store(true)
|
|
||||||
return w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Stats() sql.DBStats {
|
func (w *sqlxDBWrapper) Stats() sql.DBStats {
|
||||||
@@ -90,19 +83,19 @@ func (w *sqlxDBWrapper) Stats() sql.DBStats {
|
|||||||
func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) {
|
func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) {
|
||||||
tx, err := w.DB.Beginx()
|
tx, err := w.DB.Beginx()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, w.checkErr(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil
|
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) {
|
func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) {
|
||||||
tx, err := w.DB.BeginTxx(context.Background(), opts)
|
tx, err := w.DB.BeginTxx(context.Background(), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, w.checkErr(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil
|
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
|
func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
|
||||||
@@ -116,7 +109,7 @@ func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.checkErr(w.DB.GetContext(ctx, dest, query, args...))
|
return w.DB.GetContext(ctx, dest, query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error {
|
func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error {
|
||||||
@@ -141,7 +134,7 @@ func (w *sqlxDBWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.checkErrWithResult(w.DB.NamedExecContext(ctx, query, arg))
|
return w.DB.NamedExecContext(ctx, query, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
||||||
@@ -168,7 +161,7 @@ func (w *sqlxDBWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.checkErrWithResult(w.DB.ExecContext(context.Background(), query, args...))
|
return w.DB.ExecContext(context.Background(), query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecRaw is like Exec but without any rebinding of params. You need to pass
|
// ExecRaw is like Exec but without any rebinding of params. You need to pass
|
||||||
@@ -183,7 +176,7 @@ func (w *sqlxDBWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.checkErrWithResult(w.DB.ExecContext(ctx, query, args...))
|
return w.DB.ExecContext(ctx, query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
||||||
@@ -199,7 +192,7 @@ func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.checkErrWithRows(w.DB.NamedQueryContext(ctx, query, arg))
|
return w.DB.NamedQueryContext(ctx, query, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
||||||
@@ -227,7 +220,7 @@ func (w *sqlxDBWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.checkErrWithRows(w.DB.QueryxContext(ctx, query, args))
|
return w.DB.QueryxContext(ctx, query, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error {
|
func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error {
|
||||||
@@ -245,7 +238,7 @@ func (w *sqlxDBWrapper) SelectCtx(ctx context.Context, dest any, query string, a
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.checkErr(w.DB.SelectContext(ctx, dest, query, args...))
|
return w.DB.SelectContext(ctx, dest, query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error {
|
func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error {
|
||||||
@@ -261,15 +254,13 @@ type sqlxTxWrapper struct {
|
|||||||
*sqlx.Tx
|
*sqlx.Tx
|
||||||
queryTimeout time.Duration
|
queryTimeout time.Duration
|
||||||
trace bool
|
trace bool
|
||||||
dbw *sqlxDBWrapper
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool, dbw *sqlxDBWrapper) *sqlxTxWrapper {
|
func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool) *sqlxTxWrapper {
|
||||||
return &sqlxTxWrapper{
|
return &sqlxTxWrapper{
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
queryTimeout: timeout,
|
queryTimeout: timeout,
|
||||||
trace: trace,
|
trace: trace,
|
||||||
dbw: dbw,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +275,7 @@ func (w *sqlxTxWrapper) Get(dest any, query string, args ...any) error {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.dbw.checkErr(w.Tx.GetContext(ctx, dest, query, args...))
|
return w.Tx.GetContext(ctx, dest, query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
|
func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
|
||||||
@@ -293,13 +284,13 @@ func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.dbw.checkErr(w.Get(dest, query, args...))
|
return w.Get(dest, query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
||||||
query = w.Tx.Rebind(query)
|
query = w.Tx.Rebind(query)
|
||||||
|
|
||||||
return w.dbw.checkErrWithResult(w.ExecRaw(query, args...))
|
return w.ExecRaw(query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
|
func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
|
||||||
@@ -311,7 +302,7 @@ func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.dbw.checkErrWithResult(w.Tx.ExecContext(context.Background(), query, args...))
|
return w.Tx.ExecContext(context.Background(), query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
|
func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
|
||||||
@@ -335,7 +326,7 @@ func (w *sqlxTxWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.dbw.checkErrWithResult(w.Tx.ExecContext(ctx, query, args...))
|
return w.Tx.ExecContext(ctx, query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
||||||
@@ -351,7 +342,7 @@ func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.dbw.checkErrWithResult(w.Tx.NamedExecContext(ctx, query, arg))
|
return w.Tx.NamedExecContext(ctx, query, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
||||||
@@ -395,7 +386,7 @@ func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.rows, w.dbw.checkErr(res.err)
|
return res.rows, res.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
||||||
@@ -423,7 +414,7 @@ func (w *sqlxTxWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.dbw.checkErrWithRows(w.Tx.QueryxContext(ctx, query, args))
|
return w.Tx.QueryxContext(ctx, query, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
|
func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
|
||||||
@@ -437,7 +428,7 @@ func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.dbw.checkErr(w.Tx.SelectContext(ctx, dest, query, args...))
|
return w.Tx.SelectContext(ctx, dest, query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error {
|
func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error {
|
||||||
@@ -468,23 +459,3 @@ func printArgs(query string, dur time.Duration, args ...any) {
|
|||||||
}
|
}
|
||||||
mlog.Debug(query, fields...)
|
mlog.Debug(query, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) checkErrWithResult(res sql.Result, err error) (sql.Result, error) {
|
|
||||||
return res, w.checkErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) checkErrWithRows(res *sqlx.Rows, err error) (*sqlx.Rows, error) {
|
|
||||||
return res, w.checkErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) checkErr(err error) error {
|
|
||||||
var netError *net.OpError
|
|
||||||
if errors.As(err, &netError) && (!netError.Temporary() && !netError.Timeout()) {
|
|
||||||
w.isOnline.Store(false)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Online() bool {
|
|
||||||
return w.isOnline.Load()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package sqlstore
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -32,11 +31,9 @@ func TestSqlX(t *testing.T) {
|
|||||||
rrCounter: 0,
|
rrCounter: 0,
|
||||||
srCounter: 0,
|
srCounter: 0,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
quitMonitor: make(chan struct{}),
|
|
||||||
wgMonitor: &sync.WaitGroup{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, store.initConnection())
|
store.initConnection()
|
||||||
|
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const (
|
|||||||
MySQLForeignKeyViolationErrorCode = 1452
|
MySQLForeignKeyViolationErrorCode = 1452
|
||||||
PGDuplicateObjectErrorCode = "42710"
|
PGDuplicateObjectErrorCode = "42710"
|
||||||
MySQLDuplicateObjectErrorCode = 1022
|
MySQLDuplicateObjectErrorCode = 1022
|
||||||
DBPingAttempts = 5
|
DBPingAttempts = 18
|
||||||
DBPingTimeoutSecs = 10
|
DBPingTimeoutSecs = 10
|
||||||
// This is a numerical version string by postgres. The format is
|
// This is a numerical version string by postgres. The format is
|
||||||
// 2 characters for major, minor, and patch version prior to 10.
|
// 2 characters for major, minor, and patch version prior to 10.
|
||||||
@@ -123,9 +123,9 @@ type SqlStore struct {
|
|||||||
|
|
||||||
masterX *sqlxDBWrapper
|
masterX *sqlxDBWrapper
|
||||||
|
|
||||||
ReplicaXs []*atomic.Pointer[sqlxDBWrapper]
|
ReplicaXs []*sqlxDBWrapper
|
||||||
|
|
||||||
searchReplicaXs []*atomic.Pointer[sqlxDBWrapper]
|
searchReplicaXs []*sqlxDBWrapper
|
||||||
|
|
||||||
replicaLagHandles []*dbsql.DB
|
replicaLagHandles []*dbsql.DB
|
||||||
stores SqlStoreStores
|
stores SqlStoreStores
|
||||||
@@ -138,9 +138,6 @@ type SqlStore struct {
|
|||||||
|
|
||||||
isBinaryParam bool
|
isBinaryParam bool
|
||||||
pgDefaultTextSearchConfig string
|
pgDefaultTextSearchConfig string
|
||||||
|
|
||||||
quitMonitor chan struct{}
|
|
||||||
wgMonitor *sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlStore {
|
func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlStore {
|
||||||
@@ -149,17 +146,9 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
|
|||||||
srCounter: 0,
|
srCounter: 0,
|
||||||
settings: &settings,
|
settings: &settings,
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
quitMonitor: make(chan struct{}),
|
|
||||||
wgMonitor: &sync.WaitGroup{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := store.initConnection()
|
store.initConnection()
|
||||||
if err != nil {
|
|
||||||
mlog.Fatal("Error setting up connections", mlog.Err(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
store.wgMonitor.Add(1)
|
|
||||||
go store.monitorReplicas()
|
|
||||||
|
|
||||||
ver, err := store.GetDbVersion(true)
|
ver, err := store.GetDbVersion(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -241,28 +230,29 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
|
|||||||
|
|
||||||
// SetupConnection sets up the connection to the database and pings it to make sure it's alive.
|
// SetupConnection sets up the connection to the database and pings it to make sure it's alive.
|
||||||
// It also applies any database configuration settings that are required.
|
// It also applies any database configuration settings that are required.
|
||||||
func SetupConnection(connType string, dataSource string, settings *model.SqlSettings, attempts int) (*dbsql.DB, error) {
|
func SetupConnection(connType string, dataSource string, settings *model.SqlSettings) *dbsql.DB {
|
||||||
db, err := dbsql.Open(*settings.DriverName, dataSource)
|
db, err := dbsql.Open(*settings.DriverName, dataSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to open SQL connection")
|
mlog.Fatal("Failed to open SQL connection to err.", mlog.Err(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < attempts; i++ {
|
for i := 0; i < DBPingAttempts; i++ {
|
||||||
// At this point, we have passed sql.Open, so we deliberately ignore any errors.
|
// At this point, we have passed sql.Open, so we deliberately ignore any errors.
|
||||||
sanitized, _ := SanitizeDataSource(*settings.DriverName, dataSource)
|
sanitized, _ := SanitizeDataSource(*settings.DriverName, dataSource)
|
||||||
mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", sanitized))
|
mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", sanitized))
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err = db.PingContext(ctx)
|
err = db.PingContext(ctx)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
if i == attempts-1 {
|
break
|
||||||
return nil, err
|
} else {
|
||||||
}
|
if i == DBPingAttempts-1 {
|
||||||
|
mlog.Fatal("Failed to ping DB, server will exit.", mlog.Err(err))
|
||||||
|
} else {
|
||||||
mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs))
|
mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs))
|
||||||
time.Sleep(DBPingTimeoutSecs * time.Second)
|
time.Sleep(DBPingTimeoutSecs * time.Second)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
break
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(connType, replicaLagPrefix) {
|
if strings.HasPrefix(connType, replicaLagPrefix) {
|
||||||
@@ -282,7 +272,7 @@ func SetupConnection(connType string, dataSource string, settings *model.SqlSett
|
|||||||
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
|
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
|
||||||
db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
|
db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
|
||||||
|
|
||||||
return db, nil
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) SetContext(context context.Context) {
|
func (ss *SqlStore) SetContext(context context.Context) {
|
||||||
@@ -295,7 +285,7 @@ func (ss *SqlStore) Context() context.Context {
|
|||||||
|
|
||||||
func noOpMapper(s string) string { return s }
|
func noOpMapper(s string) string { return s }
|
||||||
|
|
||||||
func (ss *SqlStore) initConnection() error {
|
func (ss *SqlStore) initConnection() {
|
||||||
dataSource := *ss.settings.DataSource
|
dataSource := *ss.settings.DataSource
|
||||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||||
// TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout
|
// TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout
|
||||||
@@ -304,14 +294,11 @@ func (ss *SqlStore) initConnection() error {
|
|||||||
var err error
|
var err error
|
||||||
dataSource, err = ResetReadTimeout(dataSource)
|
dataSource, err = ResetReadTimeout(dataSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to reset read timeout from datasource")
|
mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(err), mlog.String("src", dataSource))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handle, err := SetupConnection("master", dataSource, ss.settings, DBPingAttempts)
|
handle := SetupConnection("master", dataSource, ss.settings)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||||
*ss.settings.Trace)
|
*ss.settings.Trace)
|
||||||
@@ -323,32 +310,34 @@ func (ss *SqlStore) initConnection() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(ss.settings.DataSourceReplicas) > 0 {
|
if len(ss.settings.DataSourceReplicas) > 0 {
|
||||||
ss.ReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceReplicas))
|
ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas))
|
||||||
for i, replica := range ss.settings.DataSourceReplicas {
|
for i, replica := range ss.settings.DataSourceReplicas {
|
||||||
ss.ReplicaXs[i] = &atomic.Pointer[sqlxDBWrapper]{}
|
handle := SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings)
|
||||||
handle, err = SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings, DBPingAttempts)
|
ss.ReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||||
if err != nil {
|
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||||
// Initializing to be offline
|
*ss.settings.Trace)
|
||||||
ss.ReplicaXs[i].Store(&sqlxDBWrapper{isOnline: &atomic.Bool{}})
|
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||||
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", fmt.Sprintf("replica-%v", i)), mlog.Err(err))
|
ss.ReplicaXs[i].MapperFunc(noOpMapper)
|
||||||
continue
|
}
|
||||||
|
if ss.metrics != nil {
|
||||||
|
ss.metrics.RegisterDBCollector(ss.ReplicaXs[i].DB.DB, "replica-"+strconv.Itoa(i))
|
||||||
}
|
}
|
||||||
ss.setDB(ss.ReplicaXs[i], handle, "replica-"+strconv.Itoa(i))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ss.settings.DataSourceSearchReplicas) > 0 {
|
if len(ss.settings.DataSourceSearchReplicas) > 0 {
|
||||||
ss.searchReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceSearchReplicas))
|
ss.searchReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceSearchReplicas))
|
||||||
for i, replica := range ss.settings.DataSourceSearchReplicas {
|
for i, replica := range ss.settings.DataSourceSearchReplicas {
|
||||||
ss.searchReplicaXs[i] = &atomic.Pointer[sqlxDBWrapper]{}
|
handle := SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings)
|
||||||
handle, err = SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings, DBPingAttempts)
|
ss.searchReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||||
if err != nil {
|
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||||
// Initializing to be offline
|
*ss.settings.Trace)
|
||||||
ss.searchReplicaXs[i].Store(&sqlxDBWrapper{isOnline: &atomic.Bool{}})
|
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||||
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", fmt.Sprintf("search-replica-%v", i)), mlog.Err(err))
|
ss.searchReplicaXs[i].MapperFunc(noOpMapper)
|
||||||
continue
|
}
|
||||||
|
if ss.metrics != nil {
|
||||||
|
ss.metrics.RegisterDBCollector(ss.searchReplicaXs[i].DB.DB, "searchreplica-"+strconv.Itoa(i))
|
||||||
}
|
}
|
||||||
ss.setDB(ss.searchReplicaXs[i], handle, "searchreplica-"+strconv.Itoa(i))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,14 +347,9 @@ func (ss *SqlStore) initConnection() error {
|
|||||||
if src.DataSource == nil {
|
if src.DataSource == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ss.replicaLagHandles[i], err = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings, DBPingAttempts)
|
ss.replicaLagHandles[i] = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings)
|
||||||
if err != nil {
|
|
||||||
mlog.Warn("Failed to setup replica lag handle. Skipping..", mlog.String("db", fmt.Sprintf(replicaLagPrefix+"-%d", i)), mlog.Err(err))
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) DriverName() string {
|
func (ss *SqlStore) DriverName() string {
|
||||||
@@ -471,15 +455,8 @@ func (ss *SqlStore) GetSearchReplicaX() *sqlxDBWrapper {
|
|||||||
return ss.GetReplicaX()
|
return ss.GetReplicaX()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(ss.searchReplicaXs); i++ {
|
|
||||||
rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs))
|
rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs))
|
||||||
if ss.searchReplicaXs[rrNum].Load().Online() {
|
return ss.searchReplicaXs[rrNum]
|
||||||
return ss.searchReplicaXs[rrNum].Load()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all search replicas are down, then go with replica.
|
|
||||||
return ss.GetReplicaX()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
|
func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
|
||||||
@@ -487,64 +464,23 @@ func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
|
|||||||
return ss.GetMasterX()
|
return ss.GetMasterX()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(ss.ReplicaXs); i++ {
|
|
||||||
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
||||||
if ss.ReplicaXs[rrNum].Load().Online() {
|
return ss.ReplicaXs[rrNum]
|
||||||
return ss.ReplicaXs[rrNum].Load()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all replicas are down, then go with master.
|
|
||||||
return ss.GetMasterX()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) monitorReplicas() {
|
func (ss *SqlStore) GetInternalReplicaDBs() []*sql.DB {
|
||||||
t := time.NewTicker(time.Duration(*ss.settings.ReplicaMonitorIntervalSeconds) * time.Second)
|
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
|
||||||
defer func() {
|
return []*sql.DB{
|
||||||
t.Stop()
|
ss.GetMasterX().DB.DB,
|
||||||
ss.wgMonitor.Done()
|
}
|
||||||
}()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ss.quitMonitor:
|
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
setupReplica := func(r *atomic.Pointer[sqlxDBWrapper], dsn, name string) {
|
|
||||||
if r.Load().Online() {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handle, err := SetupConnection(name, dsn, ss.settings, 1)
|
dbs := make([]*sql.DB, len(ss.ReplicaXs))
|
||||||
if err != nil {
|
for i, rx := range ss.ReplicaXs {
|
||||||
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", name), mlog.Err(err))
|
dbs[i] = rx.DB.DB
|
||||||
return
|
|
||||||
}
|
|
||||||
if ss.metrics != nil && r.Load() != nil && r.Load().DB != nil {
|
|
||||||
ss.metrics.UnregisterDBCollector(r.Load().DB.DB, name)
|
|
||||||
}
|
|
||||||
ss.setDB(r, handle, name)
|
|
||||||
}
|
|
||||||
for i, replica := range ss.ReplicaXs {
|
|
||||||
setupReplica(replica, ss.settings.DataSourceReplicas[i], "replica-"+strconv.Itoa(i))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, replica := range ss.searchReplicaXs {
|
return dbs
|
||||||
setupReplica(replica, ss.settings.DataSourceSearchReplicas[i], "search-replica-"+strconv.Itoa(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ss *SqlStore) setDB(replica *atomic.Pointer[sqlxDBWrapper], handle *dbsql.DB, name string) {
|
|
||||||
replica.Store(newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
|
||||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
|
||||||
*ss.settings.Trace))
|
|
||||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
|
||||||
replica.Load().MapperFunc(noOpMapper)
|
|
||||||
}
|
|
||||||
if ss.metrics != nil {
|
|
||||||
ss.metrics.RegisterDBCollector(replica.Load().DB.DB, name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
|
func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
|
||||||
@@ -553,7 +489,7 @@ func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
||||||
return ss.ReplicaXs[rrNum].Load().DB.DB
|
return ss.ReplicaXs[rrNum].DB.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) TotalMasterDbConnections() int {
|
func (ss *SqlStore) TotalMasterDbConnections() int {
|
||||||
@@ -605,10 +541,7 @@ func (ss *SqlStore) TotalReadDbConnections() int {
|
|||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
for _, db := range ss.ReplicaXs {
|
for _, db := range ss.ReplicaXs {
|
||||||
if !db.Load().Online() {
|
count = count + db.Stats().OpenConnections
|
||||||
continue
|
|
||||||
}
|
|
||||||
count = count + db.Load().Stats().OpenConnections
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
@@ -621,10 +554,7 @@ func (ss *SqlStore) TotalSearchDbConnections() int {
|
|||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
for _, db := range ss.searchReplicaXs {
|
for _, db := range ss.searchReplicaXs {
|
||||||
if !db.Load().Online() {
|
count = count + db.Stats().OpenConnections
|
||||||
continue
|
|
||||||
}
|
|
||||||
count = count + db.Load().Stats().OpenConnections
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
@@ -852,14 +782,9 @@ func IsUniqueConstraintError(err error, indexName []string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper {
|
func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper {
|
||||||
all := make([]*sqlxDBWrapper, 0, len(ss.ReplicaXs)+1)
|
all := make([]*sqlxDBWrapper, len(ss.ReplicaXs)+1)
|
||||||
for i := range ss.ReplicaXs {
|
copy(all, ss.ReplicaXs)
|
||||||
if !ss.ReplicaXs[i].Load().Online() {
|
all[len(ss.ReplicaXs)] = ss.masterX
|
||||||
continue
|
|
||||||
}
|
|
||||||
all = append(all, ss.ReplicaXs[i].Load())
|
|
||||||
}
|
|
||||||
all = append(all, ss.masterX)
|
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,24 +807,11 @@ func (ss *SqlStore) RecycleDBConnections(d time.Duration) {
|
|||||||
|
|
||||||
func (ss *SqlStore) Close() {
|
func (ss *SqlStore) Close() {
|
||||||
ss.masterX.Close()
|
ss.masterX.Close()
|
||||||
// Closing monitor and waiting for it to be done.
|
|
||||||
// This needs to be done before closing the replica handles.
|
|
||||||
close(ss.quitMonitor)
|
|
||||||
ss.wgMonitor.Wait()
|
|
||||||
|
|
||||||
for _, replica := range ss.ReplicaXs {
|
for _, replica := range ss.ReplicaXs {
|
||||||
if replica.Load().Online() {
|
replica.Close()
|
||||||
replica.Load().Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, replica := range ss.searchReplicaXs {
|
for _, replica := range ss.searchReplicaXs {
|
||||||
if replica.Load().Online() {
|
|
||||||
replica.Load().Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, replica := range ss.replicaLagHandles {
|
|
||||||
replica.Close()
|
replica.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1220,10 +1132,7 @@ func (ss *SqlStore) migrate(direction migrationDirection) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
db, err2 := SetupConnection("master", dataSource, ss.settings, DBPingAttempts)
|
db := SetupConnection("master", dataSource, ss.settings)
|
||||||
if err2 != nil {
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
driver, err = ms.WithInstance(db)
|
driver, err = ms.WithInstance(db)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
case model.DatabaseDriverPostgres:
|
case model.DatabaseDriverPostgres:
|
||||||
|
|||||||
@@ -765,11 +765,9 @@ func TestReplicaLagQuery(t *testing.T) {
|
|||||||
srCounter: 0,
|
srCounter: 0,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
metrics: mockMetrics,
|
metrics: mockMetrics,
|
||||||
quitMonitor: make(chan struct{}),
|
|
||||||
wgMonitor: &sync.WaitGroup{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, store.initConnection())
|
store.initConnection()
|
||||||
store.stores.post = newSqlPostStore(store, mockMetrics)
|
store.stores.post = newSqlPostStore(store, mockMetrics)
|
||||||
err = store.migrate(migrationsDirectionUp)
|
err = store.migrate(migrationsDirectionUp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -842,10 +840,8 @@ func TestMySQLReadTimeout(t *testing.T) {
|
|||||||
|
|
||||||
store := &SqlStore{
|
store := &SqlStore{
|
||||||
settings: settings,
|
settings: settings,
|
||||||
quitMonitor: make(chan struct{}),
|
|
||||||
wgMonitor: &sync.WaitGroup{},
|
|
||||||
}
|
}
|
||||||
require.NoError(t, store.initConnection())
|
store.initConnection()
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
_, err = store.GetMasterX().ExecNoTimeout(`SELECT SLEEP(3)`)
|
_, err = store.GetMasterX().ExecNoTimeout(`SELECT SLEEP(3)`)
|
||||||
|
|||||||
@@ -688,28 +688,6 @@ func (s *SqlThreadStore) UpdateMembership(membership *model.ThreadMembership) (*
|
|||||||
return s.updateMembership(s.GetMasterX(), membership)
|
return s.updateMembership(s.GetMasterX(), membership)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlThreadStore) DeleteMembershipsForChannel(userID, channelID string) error {
|
|
||||||
subQuery := s.getSubQueryBuilder().
|
|
||||||
Select("1").
|
|
||||||
From("Threads").
|
|
||||||
Where(sq.And{
|
|
||||||
sq.Expr("Threads.PostId = ThreadMemberships.PostId"),
|
|
||||||
sq.Eq{"Threads.ChannelId": channelID},
|
|
||||||
})
|
|
||||||
|
|
||||||
query := s.getQueryBuilder().
|
|
||||||
Delete("ThreadMemberships").
|
|
||||||
Where(sq.Eq{"UserId": userID}).
|
|
||||||
Where(sq.Expr("EXISTS (?)", subQuery))
|
|
||||||
|
|
||||||
_, err := s.GetMasterX().ExecBuilder(query)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to remove thread memberships with userid=%s channelid=%s", userID, channelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SqlThreadStore) updateMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
|
func (s *SqlThreadStore) updateMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Update("ThreadMemberships").
|
Update("ThreadMemberships").
|
||||||
@@ -734,14 +712,7 @@ func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model.
|
|||||||
memberships := []*model.ThreadMembership{}
|
memberships := []*model.ThreadMembership{}
|
||||||
|
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select(
|
Select("ThreadMemberships.*").
|
||||||
"ThreadMemberships.PostId",
|
|
||||||
"ThreadMemberships.UserId",
|
|
||||||
"ThreadMemberships.Following",
|
|
||||||
"ThreadMemberships.LastUpdated",
|
|
||||||
"ThreadMemberships.LastViewed",
|
|
||||||
"ThreadMemberships.UnreadMentions",
|
|
||||||
).
|
|
||||||
Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
|
Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
|
||||||
From("ThreadMemberships").
|
From("ThreadMemberships").
|
||||||
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
|
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
|
||||||
@@ -761,14 +732,7 @@ func (s *SqlThreadStore) GetMembershipForUser(userId, postId string) (*model.Thr
|
|||||||
func (s *SqlThreadStore) getMembershipForUser(ex sqlxExecutor, userId, postId string) (*model.ThreadMembership, error) {
|
func (s *SqlThreadStore) getMembershipForUser(ex sqlxExecutor, userId, postId string) (*model.ThreadMembership, error) {
|
||||||
var membership model.ThreadMembership
|
var membership model.ThreadMembership
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select(
|
Select("*").
|
||||||
"PostId",
|
|
||||||
"UserId",
|
|
||||||
"Following",
|
|
||||||
"LastUpdated",
|
|
||||||
"LastViewed",
|
|
||||||
"UnreadMentions",
|
|
||||||
).
|
|
||||||
From("ThreadMemberships").
|
From("ThreadMemberships").
|
||||||
Where(sq.And{
|
Where(sq.And{
|
||||||
sq.Eq{"PostId": postId},
|
sq.Eq{"PostId": postId},
|
||||||
|
|||||||
@@ -72,7 +72,10 @@ type Store interface {
|
|||||||
// GetInternalMasterDB allows access to the raw master DB
|
// GetInternalMasterDB allows access to the raw master DB
|
||||||
// handle for the multi-product architecture.
|
// handle for the multi-product architecture.
|
||||||
GetInternalMasterDB() *sql.DB
|
GetInternalMasterDB() *sql.DB
|
||||||
|
// GetInternalReplicaDBs allows access to the raw replica DB
|
||||||
|
// handles for the multi-product architecture.
|
||||||
GetInternalReplicaDB() *sql.DB
|
GetInternalReplicaDB() *sql.DB
|
||||||
|
GetInternalReplicaDBs() []*sql.DB
|
||||||
TotalMasterDbConnections() int
|
TotalMasterDbConnections() int
|
||||||
TotalReadDbConnections() int
|
TotalReadDbConnections() int
|
||||||
TotalSearchDbConnections() int
|
TotalSearchDbConnections() int
|
||||||
@@ -344,7 +347,6 @@ type ThreadStore interface {
|
|||||||
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
|
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
|
||||||
DeleteOrphanedRows(limit int) (deleted int64, err error)
|
DeleteOrphanedRows(limit int) (deleted int64, err error)
|
||||||
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
|
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
|
||||||
DeleteMembershipsForChannel(userID, channelID string) error
|
|
||||||
|
|
||||||
// Insights - threads
|
// Insights - threads
|
||||||
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
|
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
|
||||||
|
|||||||
@@ -672,38 +672,6 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []string{}, res2.Channels)
|
assert.Equal(t, []string{}, res2.Channels)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should store the correct sorting value", func(t *testing.T) {
|
|
||||||
userId := model.NewId()
|
|
||||||
|
|
||||||
team := setupTeam(t, ss, userId)
|
|
||||||
|
|
||||||
opts := &store.SidebarCategorySearchOpts{
|
|
||||||
TeamID: team.Id,
|
|
||||||
ExcludeTeam: false,
|
|
||||||
}
|
|
||||||
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
|
|
||||||
require.NoError(t, nErr)
|
|
||||||
require.NotEmpty(t, res)
|
|
||||||
// Create the category
|
|
||||||
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
|
|
||||||
SidebarCategory: model.SidebarCategory{
|
|
||||||
DisplayName: model.NewId(),
|
|
||||||
Sorting: model.SidebarCategorySortManual,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Confirm that sorting value is correct
|
|
||||||
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, res.Categories, 4)
|
|
||||||
// first category will be favorites and second will be newly created
|
|
||||||
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
|
|
||||||
assert.Equal(t, created.Id, res.Categories[1].Id)
|
|
||||||
assert.Equal(t, model.SidebarCategorySortManual, res.Categories[1].Sorting)
|
|
||||||
assert.Equal(t, model.SidebarCategorySortManual, created.Sorting)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
|
func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
|
||||||
|
|||||||
@@ -346,6 +346,22 @@ func (_m *Store) GetInternalReplicaDB() *sql.DB {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInternalReplicaDBs provides a mock function with given fields:
|
||||||
|
func (_m *Store) GetInternalReplicaDBs() []*sql.DB {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 []*sql.DB
|
||||||
|
if rf, ok := ret.Get(0).(func() []*sql.DB); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*sql.DB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// Group provides a mock function with given fields:
|
// Group provides a mock function with given fields:
|
||||||
func (_m *Store) Group() store.GroupStore {
|
func (_m *Store) Group() store.GroupStore {
|
||||||
ret := _m.Called()
|
ret := _m.Called()
|
||||||
|
|||||||
@@ -29,20 +29,6 @@ func (_m *ThreadStore) DeleteMembershipForUser(userId string, postID string) err
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMembershipsForChannel provides a mock function with given fields: userID, channelID
|
|
||||||
func (_m *ThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
|
||||||
ret := _m.Called(userID, channelID)
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
|
||||||
r0 = rf(userID, channelID)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteOrphanedRows provides a mock function with given fields: limit
|
// DeleteOrphanedRows provides a mock function with given fields: limit
|
||||||
func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||||
ret := _m.Called(limit)
|
ret := _m.Called(limit)
|
||||||
|
|||||||
@@ -261,7 +261,6 @@ func MakeSqlSettings(driver string, withReplica bool) *model.SqlSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log("Created temporary " + driver + " database " + dbName)
|
log("Created temporary " + driver + " database " + dbName)
|
||||||
settings.ReplicaMonitorIntervalSeconds = model.NewInt(5)
|
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
|
|||||||
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
|
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
|
||||||
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
|
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
|
||||||
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) })
|
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) })
|
||||||
t.Run("DeleteMembershipsForChannel", func(t *testing.T) { testDeleteMembershipsForChannel(t, ss) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testThreadStorePopulation(t *testing.T, ss store.Store) {
|
func testThreadStorePopulation(t *testing.T, ss store.Store) {
|
||||||
@@ -1915,121 +1914,3 @@ func testMarkAllAsReadByTeam(t *testing.T, ss store.Store) {
|
|||||||
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
|
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDeleteMembershipsForChannel(t *testing.T, ss store.Store) {
|
|
||||||
createThreadMembership := func(userID, postID string) (*model.ThreadMembership, func()) {
|
|
||||||
t.Helper()
|
|
||||||
opts := store.ThreadMembershipOpts{
|
|
||||||
Following: true,
|
|
||||||
IncrementMentions: false,
|
|
||||||
UpdateFollowing: true,
|
|
||||||
UpdateViewedTimestamp: false,
|
|
||||||
UpdateParticipants: false,
|
|
||||||
}
|
|
||||||
mem, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return mem, func() {
|
|
||||||
err := ss.Thread().DeleteMembershipForUser(userID, postID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
postingUserID := model.NewId()
|
|
||||||
userAID := model.NewId()
|
|
||||||
userBID := model.NewId()
|
|
||||||
|
|
||||||
team, err := ss.Team().Save(&model.Team{
|
|
||||||
DisplayName: "DisplayName",
|
|
||||||
Name: "team" + model.NewId(),
|
|
||||||
Email: MakeEmail(),
|
|
||||||
Type: model.TeamOpen,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
channel1, err := ss.Channel().Save(&model.Channel{
|
|
||||||
TeamId: team.Id,
|
|
||||||
DisplayName: "DisplayName",
|
|
||||||
Name: "channel1" + model.NewId(),
|
|
||||||
Type: model.ChannelTypeOpen,
|
|
||||||
}, -1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
channel2, err := ss.Channel().Save(&model.Channel{
|
|
||||||
TeamId: team.Id,
|
|
||||||
DisplayName: "DisplayName2",
|
|
||||||
Name: "channel2" + model.NewId(),
|
|
||||||
Type: model.ChannelTypeOpen,
|
|
||||||
}, -1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
rootPost1, err := ss.Post().Save(&model.Post{
|
|
||||||
ChannelId: channel1.Id,
|
|
||||||
UserId: postingUserID,
|
|
||||||
Message: model.NewRandomString(10),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = ss.Post().Save(&model.Post{
|
|
||||||
ChannelId: channel1.Id,
|
|
||||||
UserId: postingUserID,
|
|
||||||
Message: model.NewRandomString(10),
|
|
||||||
RootId: rootPost1.Id,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
rootPost2, err := ss.Post().Save(&model.Post{
|
|
||||||
ChannelId: channel2.Id,
|
|
||||||
UserId: postingUserID,
|
|
||||||
Message: model.NewRandomString(10),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = ss.Post().Save(&model.Post{
|
|
||||||
ChannelId: channel2.Id,
|
|
||||||
UserId: postingUserID,
|
|
||||||
Message: model.NewRandomString(10),
|
|
||||||
RootId: rootPost2.Id,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("should return memberships for user", func(t *testing.T) {
|
|
||||||
memA1, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
|
||||||
defer cleanupA1()
|
|
||||||
memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
|
||||||
defer cleanupA2()
|
|
||||||
|
|
||||||
membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, membershipsA, 2)
|
|
||||||
require.ElementsMatch(t, []*model.ThreadMembership{memA1, memA2}, membershipsA)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("should delete memberships for user for channel", func(t *testing.T) {
|
|
||||||
_, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
|
||||||
defer cleanupA1()
|
|
||||||
memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
|
||||||
defer cleanupA2()
|
|
||||||
|
|
||||||
ss.Thread().DeleteMembershipsForChannel(userAID, channel1.Id)
|
|
||||||
membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, membershipsA, 1)
|
|
||||||
require.ElementsMatch(t, []*model.ThreadMembership{memA2}, membershipsA)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("deleting memberships for channel for userA should not affect userB", func(t *testing.T) {
|
|
||||||
_, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
|
||||||
defer cleanupA1()
|
|
||||||
_, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
|
||||||
defer cleanupA2()
|
|
||||||
memB1, cleanupB2 := createThreadMembership(userBID, rootPost1.Id)
|
|
||||||
defer cleanupB2()
|
|
||||||
|
|
||||||
membershipsB, err := ss.Thread().GetMembershipsForUser(userBID, team.Id)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, membershipsB, 1)
|
|
||||||
require.ElementsMatch(t, []*model.ThreadMembership{memB1}, membershipsB)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9112,22 +9112,6 @@ func (s *TimerLayerThreadStore) DeleteMembershipForUser(userId string, postID st
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TimerLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
|
||||||
|
|
||||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
|
||||||
if s.Root.Metrics != nil {
|
|
||||||
success := "false"
|
|
||||||
if err == nil {
|
|
||||||
success = "true"
|
|
||||||
}
|
|
||||||
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.DeleteMembershipsForChannel", success, elapsed)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TimerLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
func (s *TimerLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ func (h *MainHelper) SetReplicationLagForTesting(seconds int) error {
|
|||||||
|
|
||||||
func (h *MainHelper) execOnEachReplica(query string, args ...any) error {
|
func (h *MainHelper) execOnEachReplica(query string, args ...any) error {
|
||||||
for _, replica := range h.SQLStore.ReplicaXs {
|
for _, replica := range h.SQLStore.ReplicaXs {
|
||||||
_, err := replica.Load().Exec(query, args...)
|
_, err := replica.Exec(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ func GetSanitizedClientLicense(l map[string]string) map[string]string {
|
|||||||
delete(sanitizedLicense, "StartsAt")
|
delete(sanitizedLicense, "StartsAt")
|
||||||
delete(sanitizedLicense, "ExpiresAt")
|
delete(sanitizedLicense, "ExpiresAt")
|
||||||
delete(sanitizedLicense, "SkuName")
|
delete(sanitizedLicense, "SkuName")
|
||||||
|
delete(sanitizedLicense, "SkuShortName")
|
||||||
|
|
||||||
return sanitizedLicense
|
return sanitizedLicense
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1777,10 +1777,6 @@
|
|||||||
"id": "api.error_get_first_admin_visit_marketplace_status",
|
"id": "api.error_get_first_admin_visit_marketplace_status",
|
||||||
"translation": "Error trying to retrieve the first admin visit marketplace status from the store."
|
"translation": "Error trying to retrieve the first admin visit marketplace status from the store."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "api.error_no_organization_name_provided_for_self_hosted_onboarding",
|
|
||||||
"translation": "Error no organization name provided for self hosted onboarding."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "api.error_set_first_admin_complete_setup",
|
"id": "api.error_set_first_admin_complete_setup",
|
||||||
"translation": "Error trying to save first admin complete setup in the store."
|
"translation": "Error trying to save first admin complete setup in the store."
|
||||||
|
|||||||
@@ -1173,7 +1173,6 @@ type SqlSettings struct {
|
|||||||
DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"`
|
DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"`
|
||||||
MigrationsStatementTimeoutSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
|
MigrationsStatementTimeoutSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
|
||||||
ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none
|
ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none
|
||||||
ReplicaMonitorIntervalSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlSettings) SetDefaults(isUpdate bool) {
|
func (s *SqlSettings) SetDefaults(isUpdate bool) {
|
||||||
@@ -1238,10 +1237,6 @@ func (s *SqlSettings) SetDefaults(isUpdate bool) {
|
|||||||
if s.ReplicaLagSettings == nil {
|
if s.ReplicaLagSettings == nil {
|
||||||
s.ReplicaLagSettings = []*ReplicaLagSettings{}
|
s.ReplicaLagSettings = []*ReplicaLagSettings{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ReplicaMonitorIntervalSeconds == nil {
|
|
||||||
s.ReplicaMonitorIntervalSeconds = NewInt(5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogSettings struct {
|
type LogSettings struct {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
// CompleteOnboardingRequest describes parameters of the requested plugin.
|
// CompleteOnboardingRequest describes parameters of the requested plugin.
|
||||||
type CompleteOnboardingRequest struct {
|
type CompleteOnboardingRequest struct {
|
||||||
Organization string `json:"organization"` // Organization is the name of the organization
|
|
||||||
InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed
|
InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const (
|
|||||||
SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey"
|
SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey"
|
||||||
SystemPostActionCookieSecretKey = "PostActionCookieSecret"
|
SystemPostActionCookieSecretKey = "PostActionCookieSecret"
|
||||||
SystemInstallationDateKey = "InstallationDate"
|
SystemInstallationDateKey = "InstallationDate"
|
||||||
SystemOrganizationName = "OrganizationName"
|
|
||||||
SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp"
|
SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp"
|
||||||
SystemClusterEncryptionKey = "ClusterEncryptionKey"
|
SystemClusterEncryptionKey = "ClusterEncryptionKey"
|
||||||
SystemUpgradedFromTeId = "UpgradedFromTE"
|
SystemUpgradedFromTeId = "UpgradedFromTE"
|
||||||
|
|||||||
@@ -522,7 +522,6 @@ func (ts *TelemetryService) trackConfig() {
|
|||||||
"query_timeout": *cfg.SqlSettings.QueryTimeout,
|
"query_timeout": *cfg.SqlSettings.QueryTimeout,
|
||||||
"disable_database_search": *cfg.SqlSettings.DisableDatabaseSearch,
|
"disable_database_search": *cfg.SqlSettings.DisableDatabaseSearch,
|
||||||
"migrations_statement_timeout_seconds": *cfg.SqlSettings.MigrationsStatementTimeoutSeconds,
|
"migrations_statement_timeout_seconds": *cfg.SqlSettings.MigrationsStatementTimeoutSeconds,
|
||||||
"replica_monitor_interval_seconds": *cfg.SqlSettings.ReplicaMonitorIntervalSeconds,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ts.SendTelemetry(TrackConfigLog, map[string]any{
|
ts.SendTelemetry(TrackConfigLog, map[string]any{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {Preferences} from 'mattermost-redux/constants';
|
|||||||
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getBool, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {getBool, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels';
|
import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||||
import {ChannelTypes} from 'mattermost-redux/action_types';
|
import {ChannelTypes} from 'mattermost-redux/action_types';
|
||||||
@@ -367,19 +367,11 @@ export async function redirectUserToDefaultTeam() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the user is the first admin
|
|
||||||
const isUserFirstAdmin = isFirstAdmin(state);
|
|
||||||
|
|
||||||
const locale = getCurrentLocale(state);
|
const locale = getCurrentLocale(state);
|
||||||
const teamId = LocalStorageStore.getPreviousTeamId(user.id);
|
const teamId = LocalStorageStore.getPreviousTeamId(user.id);
|
||||||
|
|
||||||
let myTeams = getMyTeams(state);
|
let myTeams = getMyTeams(state);
|
||||||
if (myTeams.length === 0) {
|
if (myTeams.length === 0) {
|
||||||
if (isUserFirstAdmin) {
|
|
||||||
getHistory().push('/preparing-workspace');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHistory().push('/select_team');
|
getHistory().push('/select_team');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {useIntl} from 'react-intl';
|
|||||||
import {useSelector, useDispatch} from 'react-redux';
|
import {useSelector, useDispatch} from 'react-redux';
|
||||||
import {useLocation, useHistory} from 'react-router-dom';
|
import {useLocation, useHistory} from 'react-router-dom';
|
||||||
|
|
||||||
|
import {redirectUserToDefaultTeam} from 'actions/global_actions';
|
||||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||||
|
|
||||||
import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg';
|
import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg';
|
||||||
@@ -14,6 +15,7 @@ import LoadingScreen from 'components/loading_screen';
|
|||||||
|
|
||||||
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
||||||
import {verifyUserEmail, getMe} from 'mattermost-redux/actions/users';
|
import {verifyUserEmail, getMe} from 'mattermost-redux/actions/users';
|
||||||
|
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ const DoVerifyEmail = () => {
|
|||||||
const token = params.get('token') ?? '';
|
const token = params.get('token') ?? '';
|
||||||
|
|
||||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||||
|
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||||
|
|
||||||
const [verifyStatus, setVerifyStatus] = useState(VerifyStatus.PENDING);
|
const [verifyStatus, setVerifyStatus] = useState(VerifyStatus.PENDING);
|
||||||
const [serverError, setServerError] = useState('');
|
const [serverError, setServerError] = useState('');
|
||||||
@@ -49,6 +52,7 @@ const DoVerifyEmail = () => {
|
|||||||
|
|
||||||
const handleRedirect = () => {
|
const handleRedirect = () => {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
|
if (useCaseOnboarding) {
|
||||||
// need info about whether admin or not,
|
// need info about whether admin or not,
|
||||||
// and whether admin has already completed
|
// and whether admin has already completed
|
||||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||||
@@ -57,6 +61,10 @@ const DoVerifyEmail = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redirectUserToDefaultTeam();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newSearchParam = new URLSearchParams(search);
|
const newSearchParam = new URLSearchParams(search);
|
||||||
newSearchParam.set('extra', Constants.SIGNIN_VERIFIED);
|
newSearchParam.set('extra', Constants.SIGNIN_VERIFIED);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {withRouter} from 'react-router-dom';
|
|||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins';
|
import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins';
|
||||||
@@ -31,6 +32,7 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
teamUrl: getCurrentRelativeTeamUrl(state),
|
teamUrl: getCurrentRelativeTeamUrl(state),
|
||||||
pluginMenuItems: getUserGuideDropdownPluginMenuItems(state),
|
pluginMenuItems: getUserGuideDropdownPluginMenuItems(state),
|
||||||
isFirstAdmin: isFirstAdmin(state),
|
isFirstAdmin: isFirstAdmin(state),
|
||||||
|
useCaseOnboarding: getUseCaseOnboarding(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ describe('components/channel_header/components/UserGuideDropdown', () => {
|
|||||||
},
|
},
|
||||||
pluginMenuItems: [],
|
pluginMenuItems: [],
|
||||||
isFirstAdmin: false,
|
isFirstAdmin: false,
|
||||||
|
useCaseOnboarding: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot', () => {
|
test('should match snapshot', () => {
|
||||||
|
|||||||
@@ -288,18 +288,4 @@ describe('components/login/Login', () => {
|
|||||||
expect(externalLoginButton.props().label).toEqual('OpenID 2');
|
expect(externalLoginButton.props().label).toEqual('OpenID 2');
|
||||||
expect(externalLoginButton.props().style).toEqual({color: '#00ff00', borderColor: '#00ff00'});
|
expect(externalLoginButton.props().style).toEqual({color: '#00ff00', borderColor: '#00ff00'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect on login', () => {
|
|
||||||
mockState.entities.users.currentUserId = 'user1';
|
|
||||||
LocalStorageStore.setWasLoggedIn(true);
|
|
||||||
mockConfig.EnableSignInWithEmail = 'true';
|
|
||||||
const redirectPath = '/boards/team/teamID/boardID';
|
|
||||||
mockLocation.search = '?redirect_to=' + redirectPath;
|
|
||||||
mount(
|
|
||||||
<MemoryRouter>
|
|
||||||
<Login/>
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith(redirectPath);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {UserProfile} from '@mattermost/types/users';
|
|||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getTeamByName, getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
import {getTeamByName, getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
import {isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||||
@@ -104,6 +104,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
const currentUser = useSelector(getCurrentUser);
|
const currentUser = useSelector(getCurrentUser);
|
||||||
const experimentalPrimaryTeam = useSelector((state: GlobalState) => (ExperimentalPrimaryTeam ? getTeamByName(state, ExperimentalPrimaryTeam) : undefined));
|
const experimentalPrimaryTeam = useSelector((state: GlobalState) => (ExperimentalPrimaryTeam ? getTeamByName(state, ExperimentalPrimaryTeam) : undefined));
|
||||||
const experimentalPrimaryTeamMember = useSelector((state: GlobalState) => getMyTeamMember(state, experimentalPrimaryTeam?.id ?? ''));
|
const experimentalPrimaryTeamMember = useSelector((state: GlobalState) => getMyTeamMember(state, experimentalPrimaryTeam?.id ?? ''));
|
||||||
|
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||||
const isCloud = useSelector(isCurrentLicenseCloud);
|
const isCloud = useSelector(isCurrentLicenseCloud);
|
||||||
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
||||||
|
|
||||||
@@ -141,9 +142,6 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableSignUpWithSaml;
|
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableSignUpWithSaml;
|
||||||
const showSignup = enableOpenServer && (enableExternalSignup || enableSignUpWithEmail || enableLdap);
|
const showSignup = enableOpenServer && (enableExternalSignup || enableSignUpWithEmail || enableLdap);
|
||||||
|
|
||||||
const query = new URLSearchParams(search);
|
|
||||||
const redirectTo = query.get('redirect_to');
|
|
||||||
|
|
||||||
const getExternalLoginOptions = () => {
|
const getExternalLoginOptions = () => {
|
||||||
const externalLoginOptions: ExternalLoginButtonType[] = [];
|
const externalLoginOptions: ExternalLoginButtonType[] = [];
|
||||||
|
|
||||||
@@ -375,10 +373,6 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
|
||||||
history.push(redirectTo);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
redirectUserToDefaultTeam();
|
redirectUserToDefaultTeam();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -622,6 +616,9 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
dispatch(setNeedsLoggedInLimitReachedCheck(true));
|
dispatch(setNeedsLoggedInLimitReachedCheck(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = new URLSearchParams(search);
|
||||||
|
const redirectTo = query.get('redirect_to');
|
||||||
|
|
||||||
setCSRFFromCookie();
|
setCSRFFromCookie();
|
||||||
|
|
||||||
// Record a successful login to local storage. If an unintentional logout occurs, e.g.
|
// Record a successful login to local storage. If an unintentional logout occurs, e.g.
|
||||||
@@ -634,12 +631,14 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
} else if (experimentalPrimaryTeamMember.team_id) {
|
} else if (experimentalPrimaryTeamMember.team_id) {
|
||||||
// Only set experimental team if user is on that team
|
// Only set experimental team if user is on that team
|
||||||
history.push(`/${ExperimentalPrimaryTeam}`);
|
history.push(`/${ExperimentalPrimaryTeam}`);
|
||||||
} else {
|
} else if (useCaseOnboarding) {
|
||||||
// need info about whether admin or not,
|
// need info about whether admin or not,
|
||||||
// and whether admin has already completed
|
// and whether admin has already completed
|
||||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||||
// let the default root component handle it.
|
// let the default root component handle it.
|
||||||
history.push('/');
|
history.push('/');
|
||||||
|
} else {
|
||||||
|
redirectUserToDefaultTeam();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`InviteMembers component should match snapshot 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="InviteMembers-body test-class"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="SingleColumnLayout"
|
|
||||||
style="width: 547px;"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="PageLine PageLine--no-left"
|
|
||||||
style="margin-bottom: 50px; margin-left: 50px; height: calc(25vh);"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
Previous step
|
|
||||||
</div>
|
|
||||||
<h1
|
|
||||||
class="PreparingWorkspaceTitle"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Invite your team members
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
class="PreparingWorkspaceDescription"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Collaboration is tough by yourself. Invite a few team members using the invitation link below.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="PreparingWorkspacePageBody"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="InviteMembersLink"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-label="team invite link"
|
|
||||||
class="InviteMembersLink__input"
|
|
||||||
data-testid="shareLinkInput"
|
|
||||||
readonly=""
|
|
||||||
type="text"
|
|
||||||
value="https://my-org.mattermost.com/config/signup_user_complete/?id=1234"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="InviteMembersLink__button"
|
|
||||||
data-testid="shareLinkInputButton"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="icon icon-link-variant"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
Copy Link
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="InviteMembers__submit"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="primary-button"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Finish setup
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="PageLine PageLine--no-left"
|
|
||||||
style="margin-top: 50px; margin-left: 50px; height: calc(30vh);"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="InviteMembersLink"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-label="team invite link"
|
|
||||||
class="InviteMembersLink__input"
|
|
||||||
data-testid="shareLinkInput"
|
|
||||||
readonly=""
|
|
||||||
type="text"
|
|
||||||
value="https://invite-url.mattermost.com"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="InviteMembersLink__button"
|
|
||||||
data-testid="shareLinkInputButton"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="icon icon-link-variant"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
Copy Link
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`components/preparing-workspace/organization_status should match snapshot 1`] = `
|
|
||||||
<div
|
|
||||||
class="Organization__status"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
|
|||||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||||
|
|
||||||
import {Action} from 'mattermost-redux/types/actions';
|
import {Action} from 'mattermost-redux/types/actions';
|
||||||
import {checkIfTeamExists, createTeam, updateTeam} from 'mattermost-redux/actions/teams';
|
import {checkIfTeamExists, createTeam} from 'mattermost-redux/actions/teams';
|
||||||
import {getProfiles} from 'mattermost-redux/actions/users';
|
import {getProfiles} from 'mattermost-redux/actions/users';
|
||||||
|
|
||||||
import PreparingWorkspace, {Actions} from './preparing_workspace';
|
import PreparingWorkspace, {Actions} from './preparing_workspace';
|
||||||
@@ -13,7 +13,6 @@ import PreparingWorkspace, {Actions} from './preparing_workspace';
|
|||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
||||||
updateTeam,
|
|
||||||
createTeam,
|
createTeam,
|
||||||
getProfiles,
|
getProfiles,
|
||||||
checkIfTeamExists,
|
checkIfTeamExists,
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
@import 'utils/mixins';
|
|
||||||
|
|
||||||
.InviteMembers-body {
|
|
||||||
display: flex;
|
|
||||||
// page width - channels preview width - progress dots width - people overlap width
|
|
||||||
max-width: calc(100vw - 600px - 120px - 30px);
|
|
||||||
|
|
||||||
.UsersEmailsInput {
|
|
||||||
max-width: 420px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.InviteMembers {
|
|
||||||
&__submit {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include simple-in-and-out-before("InviteMembers");
|
|
||||||
|
|
||||||
.ChannelsPreview--enter-from-after {
|
|
||||||
&-enter {
|
|
||||||
transform: translateX(-100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-active {
|
|
||||||
transform: translateX(0);
|
|
||||||
transition: transform 300ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-done {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ChannelsPreview--exit-to-after {
|
|
||||||
&-exit {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-exit-active {
|
|
||||||
transform: translateX(-100vw);
|
|
||||||
transition: transform 300ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-exit-done {
|
|
||||||
transform: translateX(-100vw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {ComponentProps} from 'react';
|
|
||||||
import {render, screen, fireEvent} from '@testing-library/react';
|
|
||||||
import {withIntl} from 'tests/helpers/intl-test-helper';
|
|
||||||
|
|
||||||
import InviteMembers from './invite_members';
|
|
||||||
|
|
||||||
describe('InviteMembers component', () => {
|
|
||||||
let defaultProps: ComponentProps<any>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
defaultProps = {
|
|
||||||
disableEdits: false,
|
|
||||||
browserSiteUrl: 'https://my-org.mattermost.com',
|
|
||||||
formUrl: 'https://my-org.mattermost.com/signup',
|
|
||||||
teamInviteId: '1234',
|
|
||||||
className: 'test-class',
|
|
||||||
configSiteUrl: 'https://my-org.mattermost.com/config',
|
|
||||||
onPageView: jest.fn(),
|
|
||||||
previous: <div>{'Previous step'}</div>,
|
|
||||||
next: jest.fn(),
|
|
||||||
show: true,
|
|
||||||
transitionDirection: 'forward',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should match snapshot', () => {
|
|
||||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
|
||||||
const {container} = render(component);
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders invite URL', () => {
|
|
||||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
|
||||||
render(component);
|
|
||||||
const inviteLink = screen.getByTestId('shareLinkInput');
|
|
||||||
expect(inviteLink).toHaveAttribute(
|
|
||||||
'value',
|
|
||||||
'https://my-org.mattermost.com/config/signup_user_complete/?id=1234',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders submit button with correct text', () => {
|
|
||||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
|
||||||
render(component);
|
|
||||||
const button = screen.getByRole('button', {name: 'Finish setup'});
|
|
||||||
expect(button).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('button is disabled when disableEdits is true', () => {
|
|
||||||
const component = withIntl(
|
|
||||||
<InviteMembers
|
|
||||||
{...defaultProps}
|
|
||||||
disableEdits={true}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
render(component);
|
|
||||||
const button = screen.getByRole('button', {name: 'Finish setup'});
|
|
||||||
expect(button).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('invokes next prop on button click', () => {
|
|
||||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
|
||||||
render(component);
|
|
||||||
const button = screen.getByRole('button', {name: 'Finish setup'});
|
|
||||||
fireEvent.click(button);
|
|
||||||
expect(defaultProps.next).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {useMemo, useEffect} from 'react';
|
|
||||||
import {CSSTransition} from 'react-transition-group';
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
|
||||||
|
|
||||||
import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps';
|
|
||||||
|
|
||||||
import Title from './title';
|
|
||||||
import Description from './description';
|
|
||||||
import PageBody from './page_body';
|
|
||||||
import SingleColumnLayout from './single_column_layout';
|
|
||||||
|
|
||||||
import InviteMembersLink from './invite_members_link';
|
|
||||||
import PageLine from './page_line';
|
|
||||||
import './invite_members.scss';
|
|
||||||
|
|
||||||
type Props = PreparingWorkspacePageProps & {
|
|
||||||
disableEdits: boolean;
|
|
||||||
className?: string;
|
|
||||||
teamInviteId?: string;
|
|
||||||
formUrl: Form['url'];
|
|
||||||
configSiteUrl?: string;
|
|
||||||
browserSiteUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InviteMembers = (props: Props) => {
|
|
||||||
let className = 'InviteMembers-body';
|
|
||||||
if (props.className) {
|
|
||||||
className += ' ' + props.className;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(props.onPageView, []);
|
|
||||||
|
|
||||||
const inviteURL = useMemo(() => {
|
|
||||||
let urlBase = '';
|
|
||||||
if (props.configSiteUrl && !props.configSiteUrl.includes('localhost')) {
|
|
||||||
urlBase = props.configSiteUrl;
|
|
||||||
} else if (props.formUrl && !props.formUrl.includes('localhost')) {
|
|
||||||
urlBase = props.formUrl;
|
|
||||||
} else {
|
|
||||||
urlBase = props.browserSiteUrl;
|
|
||||||
}
|
|
||||||
return `${urlBase}/signup_user_complete/?id=${props.teamInviteId}`;
|
|
||||||
}, [props.teamInviteId, props.configSiteUrl, props.browserSiteUrl, props.formUrl]);
|
|
||||||
|
|
||||||
const description = (
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.invite_members.description_link'}
|
|
||||||
defaultMessage='Collaboration is tough by yourself. Invite a few team members using the invitation link below.'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const inviteInteraction = <InviteMembersLink inviteURL={inviteURL}/>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CSSTransition
|
|
||||||
in={props.show}
|
|
||||||
timeout={Animations.PAGE_SLIDE}
|
|
||||||
classNames={mapAnimationReasonToClass('InviteMembers', props.transitionDirection)}
|
|
||||||
mountOnEnter={true}
|
|
||||||
unmountOnExit={true}
|
|
||||||
>
|
|
||||||
<div className={className}>
|
|
||||||
<SingleColumnLayout style={{width: 547}}>
|
|
||||||
<PageLine
|
|
||||||
style={{
|
|
||||||
marginBottom: '50px',
|
|
||||||
marginLeft: '50px',
|
|
||||||
height: 'calc(25vh)',
|
|
||||||
}}
|
|
||||||
noLeft={true}
|
|
||||||
/>
|
|
||||||
{props.previous}
|
|
||||||
<Title>
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.invite_members.title'}
|
|
||||||
defaultMessage='Invite your team members'
|
|
||||||
/>
|
|
||||||
</Title>
|
|
||||||
<Description>
|
|
||||||
{description}
|
|
||||||
</Description>
|
|
||||||
<PageBody>
|
|
||||||
{inviteInteraction}
|
|
||||||
</PageBody>
|
|
||||||
<div className='InviteMembers__submit'>
|
|
||||||
<button
|
|
||||||
className='primary-button'
|
|
||||||
disabled={props.disableEdits}
|
|
||||||
onClick={props.next}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.invite_members.next_link'}
|
|
||||||
defaultMessage='Finish setup'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<PageLine
|
|
||||||
style={{
|
|
||||||
marginTop: '50px',
|
|
||||||
marginLeft: '50px',
|
|
||||||
height: 'calc(30vh)',
|
|
||||||
}}
|
|
||||||
noLeft={true}
|
|
||||||
/>
|
|
||||||
</SingleColumnLayout>
|
|
||||||
</div>
|
|
||||||
</CSSTransition>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InviteMembers;
|
|
||||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -1,51 +0,0 @@
|
|||||||
.InviteMembersLink {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&__input {
|
|
||||||
height: 48px;
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
|
||||||
border-right: 0;
|
|
||||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
|
||||||
border-left: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
|
||||||
background: rgba(var(--center-channel-color-rgb), 0.04);
|
|
||||||
border-radius: 4px 0 0 4px;
|
|
||||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
display: flex;
|
|
||||||
width: 180px;
|
|
||||||
max-width: 382px;
|
|
||||||
height: 48px;
|
|
||||||
flex-grow: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid var(--button-bg);
|
|
||||||
background: var(--center-channel-bg);
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
color: var(--button-bg);
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(var(--button-bg-rgb), 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: rgba(var(--button-bg-rgb), 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
height: 24px;
|
|
||||||
margin-right: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--button-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import {render, screen, fireEvent} from '@testing-library/react';
|
|
||||||
import {trackEvent} from 'actions/telemetry_actions';
|
|
||||||
import InviteMembersLink from './invite_members_link';
|
|
||||||
import {withIntl} from 'tests/helpers/intl-test-helper';
|
|
||||||
|
|
||||||
jest.mock('actions/telemetry_actions', () => ({
|
|
||||||
trackEvent: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('components/preparing-workspace/invite_members_link', () => {
|
|
||||||
const inviteURL = 'https://invite-url.mattermost.com';
|
|
||||||
|
|
||||||
it('should match snapshot', () => {
|
|
||||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
|
||||||
|
|
||||||
const {container} = render(component);
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders an input field with the invite URL', () => {
|
|
||||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
|
||||||
render(component);
|
|
||||||
const input = screen.getByDisplayValue(inviteURL);
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders a button to copy the invite URL', () => {
|
|
||||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
|
||||||
render(component);
|
|
||||||
const button = screen.getByRole('button', {name: /copy link/i});
|
|
||||||
expect(button).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls the trackEvent function when the copy button is clicked', () => {
|
|
||||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
|
||||||
render(component);
|
|
||||||
const button = screen.getByRole('button', {name: /copy link/i});
|
|
||||||
fireEvent.click(button);
|
|
||||||
expect(trackEvent).toHaveBeenCalledWith(
|
|
||||||
'first_admin_setup',
|
|
||||||
'admin_setup_click_copy_invite_link',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('changes the button text to "Link Copied" when the URL is copied', () => {
|
|
||||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
|
||||||
render(component);
|
|
||||||
const button = screen.getByRole('button', {name: /copy link/i});
|
|
||||||
const originalText = 'Copy Link';
|
|
||||||
const linkCopiedText = 'Link Copied';
|
|
||||||
expect(button).toHaveTextContent(originalText);
|
|
||||||
|
|
||||||
fireEvent.click(button);
|
|
||||||
|
|
||||||
expect(button).toHaveTextContent(linkCopiedText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
|
||||||
|
|
||||||
import useCopyText from 'components/common/hooks/useCopyText';
|
|
||||||
import {trackEvent} from 'actions/telemetry_actions';
|
|
||||||
|
|
||||||
import './invite_members_link.scss';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
inviteURL: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InviteMembersLink = (props: Props) => {
|
|
||||||
const copyText = useCopyText({
|
|
||||||
trackCallback: () => trackEvent('first_admin_setup', 'admin_setup_click_copy_invite_link'),
|
|
||||||
text: props.inviteURL,
|
|
||||||
});
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='InviteMembersLink'>
|
|
||||||
<input
|
|
||||||
className='InviteMembersLink__input'
|
|
||||||
type='text'
|
|
||||||
readOnly={true}
|
|
||||||
value={props.inviteURL}
|
|
||||||
aria-label={intl.formatMessage({
|
|
||||||
id: 'onboarding_wizard.invite_members.copy_link_input',
|
|
||||||
defaultMessage: 'team invite link',
|
|
||||||
})}
|
|
||||||
data-testid='shareLinkInput'
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className='InviteMembersLink__button'
|
|
||||||
onClick={copyText.onClick}
|
|
||||||
data-testid='shareLinkInputButton'
|
|
||||||
>
|
|
||||||
{copyText.copiedRecently ? (
|
|
||||||
<>
|
|
||||||
<i className='icon icon-check'/>
|
|
||||||
<FormattedMessage
|
|
||||||
id='onboarding_wizard.invite_members.copied_link'
|
|
||||||
defaultMessage='Link Copied'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<i className='icon icon-link-variant'/>
|
|
||||||
<FormattedMessage
|
|
||||||
id='onboarding_wizard.invite_members.copy_link'
|
|
||||||
defaultMessage='Copy Link'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InviteMembersLink;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
@mixin input {
|
|
||||||
width: 452px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 2px solid rgba(var(--center-channel-color-rgb), 0.16);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
border: 2px solid var(--button-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
@import 'utils/variables';
|
|
||||||
@import 'utils/mixins';
|
|
||||||
@import './mixins';
|
|
||||||
|
|
||||||
.Organization-body {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Organization-form-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Organization-left-col {
|
|
||||||
width: 210px;
|
|
||||||
min-width: 210px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Organization-right-col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Organization {
|
|
||||||
&__input {
|
|
||||||
@include input;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
&--error {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: var(--dnd-indicator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__progress-path {
|
|
||||||
position: absolute;
|
|
||||||
top: -25px;
|
|
||||||
left: -55px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
margin-left: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 700px) {
|
|
||||||
.Organization-left-col {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include simple-in-and-out("Organization");
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {useState, useEffect, useRef, ChangeEvent} from 'react';
|
|
||||||
import {CSSTransition} from 'react-transition-group';
|
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
|
||||||
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
|
|
||||||
import OrganizationSVG from 'components/common/svg_images_components/organization-building_svg';
|
|
||||||
import QuickInput from 'components/quick_input';
|
|
||||||
|
|
||||||
import {trackEvent} from 'actions/telemetry_actions';
|
|
||||||
|
|
||||||
import {getTeams} from 'mattermost-redux/actions/teams';
|
|
||||||
import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
|
||||||
import {Team} from '@mattermost/types/teams';
|
|
||||||
|
|
||||||
import {teamNameToUrl} from 'utils/url';
|
|
||||||
import Constants from 'utils/constants';
|
|
||||||
|
|
||||||
import OrganizationStatus, {TeamApiError} from './organization_status';
|
|
||||||
import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps';
|
|
||||||
import PageLine from './page_line';
|
|
||||||
import Title from './title';
|
|
||||||
import Description from './description';
|
|
||||||
import PageBody from './page_body';
|
|
||||||
|
|
||||||
import './organization.scss';
|
|
||||||
|
|
||||||
type Props = PreparingWorkspacePageProps & {
|
|
||||||
organization: Form['organization'];
|
|
||||||
setOrganization: (organization: Form['organization']) => void;
|
|
||||||
className?: string;
|
|
||||||
createTeam: (OrganizationName: string) => Promise<{error: string | null; newTeam: Team | null}>;
|
|
||||||
updateTeam: (teamToUpdate: Team) => Promise<{error: string | null; updatedTeam: Team | null}>;
|
|
||||||
setInviteId: (inviteId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reportValidationError = debounce(() => {
|
|
||||||
trackEvent('first_admin_setup', 'validate_organization_error');
|
|
||||||
}, 700, {leading: false});
|
|
||||||
|
|
||||||
const Organization = (props: Props) => {
|
|
||||||
const {formatMessage} = useIntl();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const [triedNext, setTriedNext] = useState(false);
|
|
||||||
const inputRef = useRef<HTMLInputElement>();
|
|
||||||
const validation = teamNameToUrl(props.organization || '');
|
|
||||||
const teamApiError = useRef<typeof TeamApiError | null>(null);
|
|
||||||
|
|
||||||
useEffect(props.onPageView, []);
|
|
||||||
|
|
||||||
const teams = useSelector(getActiveTeamsList);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!teams) {
|
|
||||||
dispatch(getTeams(0, 60));
|
|
||||||
}
|
|
||||||
}, [teams]);
|
|
||||||
|
|
||||||
const setApiCallError = () => {
|
|
||||||
teamApiError.current = TeamApiError;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTeamNameFromOrgName = async () => {
|
|
||||||
if (!inputRef.current?.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const name = inputRef.current?.value.trim();
|
|
||||||
|
|
||||||
const currentTeam = teams[0];
|
|
||||||
|
|
||||||
if (currentTeam && name && name !== currentTeam.display_name) {
|
|
||||||
const {error} = await props.updateTeam({...currentTeam, display_name: name});
|
|
||||||
if (error !== null) {
|
|
||||||
setApiCallError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTeamFromOrgName = async () => {
|
|
||||||
if (!inputRef.current?.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const name = inputRef.current?.value.trim();
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
const {error, newTeam} = await props.createTeam(name);
|
|
||||||
if (error !== null || newTeam === null) {
|
|
||||||
props.setInviteId('');
|
|
||||||
setApiCallError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
props.setInviteId(newTeam.invite_id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
props.setOrganization(e.target.value);
|
|
||||||
teamApiError.current = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNext = (e?: React.KeyboardEvent | React.MouseEvent) => {
|
|
||||||
if (e && (e as React.KeyboardEvent).key) {
|
|
||||||
if ((e as React.KeyboardEvent).key !== Constants.KeyCodes.ENTER[0]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!triedNext) {
|
|
||||||
setTriedNext(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is already a team, maybe because a page reload, then just update the teamname
|
|
||||||
const thereIsAlreadyATeam = teams.length > 0;
|
|
||||||
teamApiError.current = null;
|
|
||||||
|
|
||||||
if (!validation.error && !thereIsAlreadyATeam) {
|
|
||||||
createTeamFromOrgName();
|
|
||||||
} else if (!validation.error && thereIsAlreadyATeam) {
|
|
||||||
updateTeamNameFromOrgName();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validation.error || teamApiError.current) {
|
|
||||||
reportValidationError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
props.next?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
let className = 'Organization-body';
|
|
||||||
if (props.className) {
|
|
||||||
className += ' ' + props.className;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<CSSTransition
|
|
||||||
in={props.show}
|
|
||||||
timeout={Animations.PAGE_SLIDE}
|
|
||||||
classNames={mapAnimationReasonToClass('Organization', props.transitionDirection)}
|
|
||||||
mountOnEnter={true}
|
|
||||||
unmountOnExit={true}
|
|
||||||
>
|
|
||||||
<div className={className}>
|
|
||||||
<div className='Organization-right-col'>
|
|
||||||
<div className='Organization-form-wrapper'>
|
|
||||||
<div className='Organization__progress-path'>
|
|
||||||
<OrganizationSVG/>
|
|
||||||
<PageLine
|
|
||||||
style={{
|
|
||||||
marginTop: '5px',
|
|
||||||
height: 'calc(50vh)',
|
|
||||||
}}
|
|
||||||
noLeft={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='Organization__content'>
|
|
||||||
{props.previous}
|
|
||||||
<Title>
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.organization.title'}
|
|
||||||
defaultMessage='What’s the name of your organization?'
|
|
||||||
/>
|
|
||||||
</Title>
|
|
||||||
<Description>
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.organization.description'}
|
|
||||||
defaultMessage='We’ll use this to help personalize your workspace.'
|
|
||||||
/>
|
|
||||||
</Description>
|
|
||||||
<PageBody>
|
|
||||||
<QuickInput
|
|
||||||
placeholder={
|
|
||||||
formatMessage({
|
|
||||||
id: 'onboarding_wizard.organization.placeholder',
|
|
||||||
defaultMessage: 'Organization name',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className='Organization__input'
|
|
||||||
value={props.organization || ''}
|
|
||||||
onChange={(e) => handleOnChange(e)}
|
|
||||||
onKeyUp={onNext}
|
|
||||||
autoFocus={true}
|
|
||||||
ref={inputRef as unknown as any}
|
|
||||||
/>
|
|
||||||
{triedNext ? <OrganizationStatus error={validation.error || teamApiError.current}/> : null}
|
|
||||||
</PageBody>
|
|
||||||
<button
|
|
||||||
className='primary-button'
|
|
||||||
data-testid='continue'
|
|
||||||
onClick={onNext}
|
|
||||||
disabled={!props.organization}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.next'}
|
|
||||||
defaultMessage='Continue'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CSSTransition>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Organization;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import {render} from '@testing-library/react';
|
|
||||||
import {BadUrlReasons} from 'utils/url';
|
|
||||||
import OrganizationStatus, {TeamApiError} from './organization_status';
|
|
||||||
import {withIntl} from 'tests/helpers/intl-test-helper';
|
|
||||||
|
|
||||||
describe('components/preparing-workspace/organization_status', () => {
|
|
||||||
const defaultProps = {
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should match snapshot', () => {
|
|
||||||
const {container} = render(<OrganizationStatus {...defaultProps}/>);
|
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render no error message when error prop is null', () => {
|
|
||||||
const {queryByText, container} = render(<OrganizationStatus {...defaultProps}/>);
|
|
||||||
expect((container.getElementsByClassName('Organization__status').length)).toBe(1);
|
|
||||||
expect(queryByText(/empty/i)).not.toBeInTheDocument();
|
|
||||||
expect(queryByText(/team api error/i)).not.toBeInTheDocument();
|
|
||||||
expect(queryByText(/length/i)).not.toBeInTheDocument();
|
|
||||||
expect(queryByText(/reserved/i)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render an error message for an empty organization name', () => {
|
|
||||||
const component = withIntl(<OrganizationStatus error={BadUrlReasons.Empty}/>);
|
|
||||||
const {getByText} = render(component);
|
|
||||||
expect(getByText(/You must enter an organization name/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render an error message for a team API error', () => {
|
|
||||||
const component = withIntl(<OrganizationStatus error={TeamApiError}/>);
|
|
||||||
const {getByText} = render(component);
|
|
||||||
expect(getByText(/There was an error, please try again/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render an error message for an organization name with invalid length', () => {
|
|
||||||
const component = withIntl(<OrganizationStatus error={BadUrlReasons.Length}/>);
|
|
||||||
const {getByText} = render(component);
|
|
||||||
expect(getByText(/Organization name must be between 2 and 64 characters/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
|
||||||
|
|
||||||
import {BadUrlReasons, UrlValidationCheck} from 'utils/url';
|
|
||||||
import Constants, {DocLinks} from 'utils/constants';
|
|
||||||
import ExternalLink from 'components/external_link';
|
|
||||||
|
|
||||||
export const TeamApiError = 'team_api_error';
|
|
||||||
|
|
||||||
const OrganizationStatus = (props: {error: (UrlValidationCheck['error'] | typeof TeamApiError | null)}): JSX.Element => {
|
|
||||||
let children = null;
|
|
||||||
let className = 'Organization__status';
|
|
||||||
if (props.error) {
|
|
||||||
className += ' Organization__status--error';
|
|
||||||
switch (props.error) {
|
|
||||||
case BadUrlReasons.Empty:
|
|
||||||
children = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='onboarding_wizard.organization.empty'
|
|
||||||
defaultMessage='You must enter an organization name'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case TeamApiError:
|
|
||||||
children = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='onboarding_wizard.organization.team_api_error'
|
|
||||||
defaultMessage='There was an error, please try again.'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case BadUrlReasons.Length:
|
|
||||||
children = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='onboarding_wizard.organization.length'
|
|
||||||
defaultMessage='Organization name must be between {min} and {max} characters'
|
|
||||||
values={{
|
|
||||||
min: Constants.MIN_TEAMNAME_LENGTH,
|
|
||||||
max: Constants.MAX_TEAMNAME_LENGTH,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case BadUrlReasons.Reserved:
|
|
||||||
children = (
|
|
||||||
<FormattedMessage
|
|
||||||
|
|
||||||
id='onboarding_wizard.organization.reserved'
|
|
||||||
defaultMessage='Organization name may not <a>start with a reserved word</a>.'
|
|
||||||
values={{
|
|
||||||
a: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
|
||||||
<ExternalLink
|
|
||||||
href={DocLinks.ABOUT_TEAMS}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
{chunks}
|
|
||||||
</ExternalLink>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
children = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='onboarding_wizard.organization.other'
|
|
||||||
defaultMessage='Invalid organization name: {reason}'
|
|
||||||
values={{
|
|
||||||
reason: props.error,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <div className={className}>{children}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OrganizationStatus;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
.PageLine {
|
|
||||||
position: relative;
|
|
||||||
left: 100px;
|
|
||||||
width: 1px;
|
|
||||||
background-color: rgba(var(--center-channel-color-rgb), 0.24);
|
|
||||||
|
|
||||||
&--no-left {
|
|
||||||
left: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import './page_line.scss';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
style?: Record<string, string>;
|
|
||||||
noLeft?: boolean;
|
|
||||||
}
|
|
||||||
const PageLine = (props: Props) => {
|
|
||||||
let className = 'PageLine';
|
|
||||||
if (props.noLeft) {
|
|
||||||
className += ' PageLine--no-left';
|
|
||||||
}
|
|
||||||
const styles: Record<string, string> = {};
|
|
||||||
if (props?.style) {
|
|
||||||
Object.assign(styles, props.style);
|
|
||||||
}
|
|
||||||
if (!styles.height) {
|
|
||||||
styles.height = '100vh';
|
|
||||||
}
|
|
||||||
if ((!props.style?.height && styles.height === '100vh') && !styles.marginTop) {
|
|
||||||
styles.marginTop = '50px';
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
style={styles}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageLine;
|
|
||||||
@@ -4,9 +4,6 @@
|
|||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugins-skip-btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
// preempt cards wrapping
|
// preempt cards wrapping
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
.Plugins-body {
|
.Plugins-body {
|
||||||
|
|||||||
@@ -21,16 +21,15 @@ import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps
|
|||||||
import Title from './title';
|
import Title from './title';
|
||||||
import Description from './description';
|
import Description from './description';
|
||||||
import PageBody from './page_body';
|
import PageBody from './page_body';
|
||||||
|
|
||||||
import SingleColumnLayout from './single_column_layout';
|
import SingleColumnLayout from './single_column_layout';
|
||||||
|
|
||||||
import PageLine from './page_line';
|
|
||||||
import './plugins.scss';
|
import './plugins.scss';
|
||||||
|
|
||||||
type Props = PreparingWorkspacePageProps & {
|
type Props = PreparingWorkspacePageProps & {
|
||||||
options: Form['plugins'];
|
options: Form['plugins'];
|
||||||
setOption: (option: keyof Form['plugins']) => void;
|
setOption: (option: keyof Form['plugins']) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
isSelfHosted: boolean;
|
|
||||||
}
|
}
|
||||||
const Plugins = (props: Props) => {
|
const Plugins = (props: Props) => {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
@@ -45,34 +44,6 @@ const Plugins = (props: Props) => {
|
|||||||
if (props.className) {
|
if (props.className) {
|
||||||
className += ' ' + props.className;
|
className += ' ' + props.className;
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = (
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.cloud_plugins.title'}
|
|
||||||
defaultMessage='Welcome to Mattermost!'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
let description = (
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.cloud_plugins.description'}
|
|
||||||
defaultMessage={'Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
if (props.isSelfHosted) {
|
|
||||||
title = (
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.self_hosted_plugins.title'}
|
|
||||||
defaultMessage='What tools do you use?'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
description = (
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.self_hosted_plugins.description'}
|
|
||||||
defaultMessage={'Choose the tools you work with, and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
in={props.show}
|
in={props.show}
|
||||||
@@ -83,29 +54,26 @@ const Plugins = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<SingleColumnLayout>
|
<SingleColumnLayout>
|
||||||
<PageLine
|
|
||||||
style={{
|
|
||||||
marginBottom: '50px',
|
|
||||||
marginLeft: '50px',
|
|
||||||
height: 'calc(25vh)',
|
|
||||||
}}
|
|
||||||
noLeft={true}
|
|
||||||
/>
|
|
||||||
{props.previous}
|
{props.previous}
|
||||||
<Title>
|
<Title>
|
||||||
{title}
|
<FormattedMessage
|
||||||
{!props.isSelfHosted && (
|
id={'onboarding_wizard.plugins.title'}
|
||||||
|
defaultMessage='Welcome to Mattermost!'
|
||||||
|
/>
|
||||||
<div className='subtitle'>
|
<div className='subtitle'>
|
||||||
<CelebrateSVG/>
|
<CelebrateSVG/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id={'onboarding_wizard.cloud_plugins.subtitle'}
|
id={'onboarding_wizard.plugins.subtitle'}
|
||||||
defaultMessage='(almost there!)'
|
defaultMessage='(almost there!)'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
|
||||||
</Title>
|
</Title>
|
||||||
<Description>{description}</Description>
|
<Description>
|
||||||
|
<FormattedMessage
|
||||||
|
id={'onboarding_wizard.plugins.description'}
|
||||||
|
defaultMessage={'Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
||||||
|
/>
|
||||||
|
</Description>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<MultiSelectCards
|
<MultiSelectCards
|
||||||
size='small'
|
size='small'
|
||||||
@@ -198,23 +166,15 @@ const Plugins = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className='link-style plugins-skip-btn'
|
className='tertiary-button'
|
||||||
onClick={props.skip}
|
onClick={props.skip}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id={'onboarding_wizard.skip-button'}
|
id={'onboarding_wizard.skip'}
|
||||||
defaultMessage='Skip'
|
defaultMessage='Skip for now'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<PageLine
|
|
||||||
style={{
|
|
||||||
marginTop: '50px',
|
|
||||||
marginLeft: '50px',
|
|
||||||
height: 'calc(30vh)',
|
|
||||||
}}
|
|
||||||
noLeft={true}
|
|
||||||
/>
|
|
||||||
</SingleColumnLayout>
|
</SingleColumnLayout>
|
||||||
</div>
|
</div>
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
|
|||||||
@@ -63,21 +63,6 @@
|
|||||||
.primary-button {
|
.primary-button {
|
||||||
@include primary-button;
|
@include primary-button;
|
||||||
@include button-medium;
|
@include button-medium;
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 2px solid var(--button-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-button[disabled] {
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 2px solid rgba(var(--center-channel-color-rgb), 0.01);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-style {
|
|
||||||
@include link;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.child-page {
|
.child-page {
|
||||||
@@ -85,43 +70,6 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__invite-members-illustration {
|
|
||||||
position: absolute;
|
|
||||||
top: 25%;
|
|
||||||
right: -651px;
|
|
||||||
animation-duration: 0.3s;
|
|
||||||
animation-fill-mode: forwards;
|
|
||||||
animation-timing-function: ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.enter {
|
|
||||||
animation-name: slideInRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exit {
|
|
||||||
animation-name: slideOutRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInRight {
|
|
||||||
from {
|
|
||||||
right: -651px;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideOutRight {
|
|
||||||
from {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
right: -651px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.PreparingWorkspacePageContainer {
|
.PreparingWorkspacePageContainer {
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useState, useCallback, useEffect, useRef, useMemo} from 'react';
|
import React, {useState, useCallback, useEffect, useRef} from 'react';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
import {RouterProps} from 'react-router-dom';
|
import {RouterProps} from 'react-router-dom';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||||
import {General} from 'mattermost-redux/constants';
|
import {General} from 'mattermost-redux/constants';
|
||||||
import {getFirstAdminSetupComplete as getFirstAdminSetupCompleteAction} from 'mattermost-redux/actions/general';
|
import {getFirstAdminSetupComplete as getFirstAdminSetupCompleteAction} from 'mattermost-redux/actions/general';
|
||||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
import {Team} from '@mattermost/types/teams';
|
import {Team} from '@mattermost/types/teams';
|
||||||
|
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {getCurrentTeam, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeam, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getFirstAdminSetupComplete, getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
import {getFirstAdminSetupComplete, getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
import {getSiteURL, teamNameToUrl} from 'utils/url';
|
|
||||||
import {makeNewTeam} from 'utils/team_utils';
|
|
||||||
|
|
||||||
import {pageVisited, trackEvent} from 'actions/telemetry_actions';
|
import {pageVisited, trackEvent} from 'actions/telemetry_actions';
|
||||||
|
|
||||||
@@ -36,14 +35,10 @@ import {
|
|||||||
mapStepToPageView,
|
mapStepToPageView,
|
||||||
mapStepToSubmitFail,
|
mapStepToSubmitFail,
|
||||||
PLUGIN_NAME_TO_ID_MAP,
|
PLUGIN_NAME_TO_ID_MAP,
|
||||||
mapStepToPrevious,
|
|
||||||
} from './steps';
|
} from './steps';
|
||||||
|
|
||||||
import Organization from './organization';
|
|
||||||
import Plugins from './plugins';
|
import Plugins from './plugins';
|
||||||
import Progress from './progress';
|
import Progress from './progress';
|
||||||
import InviteMembers from './invite_members';
|
|
||||||
import InviteMembersIllustration from './invite_members_illustration';
|
|
||||||
import LaunchingWorkspace, {START_TRANSITIONING_OUT} from './launching_workspace';
|
import LaunchingWorkspace, {START_TRANSITIONING_OUT} from './launching_workspace';
|
||||||
|
|
||||||
import './preparing_workspace.scss';
|
import './preparing_workspace.scss';
|
||||||
@@ -63,7 +58,6 @@ const WAIT_FOR_REDIRECT_TIME = 2000 - START_TRANSITIONING_OUT;
|
|||||||
|
|
||||||
export type Actions = {
|
export type Actions = {
|
||||||
createTeam: (team: Team) => ActionResult;
|
createTeam: (team: Team) => ActionResult;
|
||||||
updateTeam: (team: Team) => ActionResult;
|
|
||||||
checkIfTeamExists: (teamName: string) => ActionResult;
|
checkIfTeamExists: (teamName: string) => ActionResult;
|
||||||
getProfiles: (page: number, perPage: number, options: Record<string, any>) => ActionResult;
|
getProfiles: (page: number, perPage: number, options: Record<string, any>) => ActionResult;
|
||||||
}
|
}
|
||||||
@@ -87,16 +81,12 @@ function makeSubmitFail(step: WizardStep) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trackSubmitFail = {
|
const trackSubmitFail = {
|
||||||
[WizardSteps.Organization]: makeSubmitFail(WizardSteps.Organization),
|
|
||||||
[WizardSteps.Plugins]: makeSubmitFail(WizardSteps.Plugins),
|
[WizardSteps.Plugins]: makeSubmitFail(WizardSteps.Plugins),
|
||||||
[WizardSteps.InviteMembers]: makeSubmitFail(WizardSteps.InviteMembers),
|
|
||||||
[WizardSteps.LaunchingWorkspace]: makeSubmitFail(WizardSteps.LaunchingWorkspace),
|
[WizardSteps.LaunchingWorkspace]: makeSubmitFail(WizardSteps.LaunchingWorkspace),
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPageViews = {
|
const onPageViews = {
|
||||||
[WizardSteps.Organization]: makeOnPageView(WizardSteps.Organization),
|
|
||||||
[WizardSteps.Plugins]: makeOnPageView(WizardSteps.Plugins),
|
[WizardSteps.Plugins]: makeOnPageView(WizardSteps.Plugins),
|
||||||
[WizardSteps.InviteMembers]: makeOnPageView(WizardSteps.InviteMembers),
|
|
||||||
[WizardSteps.LaunchingWorkspace]: makeOnPageView(WizardSteps.LaunchingWorkspace),
|
[WizardSteps.LaunchingWorkspace]: makeOnPageView(WizardSteps.LaunchingWorkspace),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,35 +98,28 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
defaultMessage: 'Something went wrong. Please try again.',
|
defaultMessage: 'Something went wrong. Please try again.',
|
||||||
});
|
});
|
||||||
const isUserFirstAdmin = useSelector(isFirstAdmin);
|
const isUserFirstAdmin = useSelector(isFirstAdmin);
|
||||||
|
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||||
|
|
||||||
const currentTeam = useSelector(getCurrentTeam);
|
const currentTeam = useSelector(getCurrentTeam);
|
||||||
const myTeams = useSelector(getMyTeams);
|
const myTeams = useSelector(getMyTeams);
|
||||||
|
|
||||||
// In cloud instances created from portal,
|
// In cloud instances created from portal,
|
||||||
// new admin user has a team in myTeams but not in currentTeam.
|
// new admin user has a team in myTeams but not in currentTeam.
|
||||||
let team = currentTeam || myTeams?.[0];
|
const team = currentTeam || myTeams?.[0];
|
||||||
|
|
||||||
const config = useSelector(getConfig);
|
const config = useSelector(getConfig);
|
||||||
const pluginsEnabled = config.PluginsEnabled === 'true';
|
const pluginsEnabled = config.PluginsEnabled === 'true';
|
||||||
const showOnMountTimeout = useRef<NodeJS.Timeout>();
|
const showOnMountTimeout = useRef<NodeJS.Timeout>();
|
||||||
const configSiteUrl = config.SiteURL;
|
|
||||||
const isSelfHosted = useSelector(getLicense).Cloud !== 'true';
|
|
||||||
|
|
||||||
const stepOrder = [
|
const stepOrder = [
|
||||||
isSelfHosted && WizardSteps.Organization,
|
|
||||||
pluginsEnabled && WizardSteps.Plugins,
|
pluginsEnabled && WizardSteps.Plugins,
|
||||||
isSelfHosted && WizardSteps.InviteMembers,
|
|
||||||
WizardSteps.LaunchingWorkspace,
|
WizardSteps.LaunchingWorkspace,
|
||||||
].filter((x) => Boolean(x)) as WizardStep[];
|
].filter((x) => Boolean(x)) as WizardStep[];
|
||||||
|
|
||||||
// first steporder that is not false
|
|
||||||
const firstShowablePage = stepOrder[0];
|
|
||||||
|
|
||||||
const firstAdminSetupComplete = useSelector(getFirstAdminSetupComplete);
|
const firstAdminSetupComplete = useSelector(getFirstAdminSetupComplete);
|
||||||
|
|
||||||
const [[mostRecentStep, currentStep], setStepHistory] = useState<[WizardStep, WizardStep]>([stepOrder[0], stepOrder[0]]);
|
const [[mostRecentStep, currentStep], setStepHistory] = useState<[WizardStep, WizardStep]>([stepOrder[0], stepOrder[0]]);
|
||||||
const [submissionState, setSubmissionState] = useState<SubmissionState>(SubmissionStates.Presubmit);
|
const [submissionState, setSubmissionState] = useState<SubmissionState>(SubmissionStates.Presubmit);
|
||||||
const browserSiteUrl = useMemo(getSiteURL, []);
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
...emptyForm,
|
...emptyForm,
|
||||||
});
|
});
|
||||||
@@ -205,44 +188,13 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
trackSubmitFail[redirectTo]();
|
trackSubmitFail[redirectTo]();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const createTeam = async (OrganizationName: string): Promise<{error: string | null; newTeam: Team | null}> => {
|
|
||||||
const data = await props.actions.createTeam(makeNewTeam(OrganizationName, teamNameToUrl(OrganizationName || '').url));
|
|
||||||
if (data.error) {
|
|
||||||
return {error: genericSubmitError, newTeam: null};
|
|
||||||
}
|
|
||||||
return {error: null, newTeam: data.data};
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTeam = async (teamToUpdate: Team): Promise<{error: string | null; updatedTeam: Team | null}> => {
|
|
||||||
const data = await props.actions.updateTeam(teamToUpdate);
|
|
||||||
if (data.error) {
|
|
||||||
return {error: genericSubmitError, updatedTeam: null};
|
|
||||||
}
|
|
||||||
return {error: null, updatedTeam: data.data};
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendForm = async () => {
|
const sendForm = async () => {
|
||||||
const sendFormStart = Date.now();
|
const sendFormStart = Date.now();
|
||||||
setSubmissionState(SubmissionStates.Submitting);
|
setSubmissionState(SubmissionStates.Submitting);
|
||||||
|
|
||||||
if (form.organization && !isSelfHosted) {
|
|
||||||
try {
|
|
||||||
const {error, newTeam} = await createTeam(form.organization);
|
|
||||||
if (error !== null) {
|
|
||||||
redirectWithError(WizardSteps.Organization, genericSubmitError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
team = newTeam as Team;
|
|
||||||
} catch (e) {
|
|
||||||
redirectWithError(WizardSteps.Organization, genericSubmitError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// send plugins
|
// send plugins
|
||||||
const {skipped: skippedPlugins, ...pluginChoices} = form.plugins;
|
const {skipped: skippedPlugins, ...pluginChoices} = form.plugins;
|
||||||
let pluginsToSetup: string[] = [];
|
let pluginsToSetup: string[] = [];
|
||||||
|
|
||||||
if (!skippedPlugins) {
|
if (!skippedPlugins) {
|
||||||
pluginsToSetup = Object.entries(pluginChoices).reduce(
|
pluginsToSetup = Object.entries(pluginChoices).reduce(
|
||||||
(acc: string[], [k, v]): string[] => (v ? [...acc, PLUGIN_NAME_TO_ID_MAP[k as keyof Omit<Form['plugins'], 'skipped'>]] : acc), [],
|
(acc: string[], [k, v]): string[] => (v ? [...acc, PLUGIN_NAME_TO_ID_MAP[k as keyof Omit<Form['plugins'], 'skipped'>]] : acc), [],
|
||||||
@@ -252,10 +204,8 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
// This endpoint sets setup complete state, so we need to make this request
|
// This endpoint sets setup complete state, so we need to make this request
|
||||||
// even if admin skipped submitting plugins.
|
// even if admin skipped submitting plugins.
|
||||||
const completeSetupRequest = {
|
const completeSetupRequest = {
|
||||||
organization: form.organization,
|
|
||||||
install_plugins: pluginsToSetup,
|
install_plugins: pluginsToSetup,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Client4.completeSetup(completeSetupRequest);
|
await Client4.completeSetup(completeSetupRequest);
|
||||||
dispatch({type: GeneralTypes.FIRST_ADMIN_COMPLETE_SETUP_RECEIVED, data: true});
|
dispatch({type: GeneralTypes.FIRST_ADMIN_COMPLETE_SETUP_RECEIVED, data: true});
|
||||||
@@ -271,7 +221,6 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
|
|
||||||
const sendFormEnd = Date.now();
|
const sendFormEnd = Date.now();
|
||||||
const timeToWait = WAIT_FOR_REDIRECT_TIME - (sendFormEnd - sendFormStart);
|
const timeToWait = WAIT_FOR_REDIRECT_TIME - (sendFormEnd - sendFormStart);
|
||||||
|
|
||||||
if (timeToWait > 0) {
|
if (timeToWait > 0) {
|
||||||
setTimeout(goToChannels, timeToWait);
|
setTimeout(goToChannels, timeToWait);
|
||||||
} else {
|
} else {
|
||||||
@@ -287,8 +236,7 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
}, [submissionState]);
|
}, [submissionState]);
|
||||||
|
|
||||||
const adminRevisitedPage = firstAdminSetupComplete && submissionState === SubmissionStates.Presubmit;
|
const adminRevisitedPage = firstAdminSetupComplete && submissionState === SubmissionStates.Presubmit;
|
||||||
const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage;
|
const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage || !useCaseOnboarding;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldRedirect) {
|
if (shouldRedirect) {
|
||||||
props.history.push('/');
|
props.history.push('/');
|
||||||
@@ -308,24 +256,6 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
return stepIndex > currentStepIndex ? Animations.Reasons.ExitToBefore : Animations.Reasons.ExitToAfter;
|
return stepIndex > currentStepIndex ? Animations.Reasons.ExitToBefore : Animations.Reasons.ExitToAfter;
|
||||||
};
|
};
|
||||||
|
|
||||||
const goPrevious = useCallback((e?: React.KeyboardEvent | React.MouseEvent) => {
|
|
||||||
if (e && (e as React.KeyboardEvent).key) {
|
|
||||||
const key = (e as React.KeyboardEvent).key;
|
|
||||||
if (key !== Constants.KeyCodes.ENTER[0] && key !== Constants.KeyCodes.SPACE[0]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (submissionState !== SubmissionStates.Presubmit && submissionState !== SubmissionStates.SubmitFail) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const stepIndex = stepOrder.indexOf(currentStep);
|
|
||||||
if (stepIndex <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
trackEvent('first_admin_setup', mapStepToPrevious(currentStep));
|
|
||||||
setStepHistory([currentStep, stepOrder[stepIndex - 1]]);
|
|
||||||
}, [currentStep]);
|
|
||||||
|
|
||||||
const skipPlugins = useCallback((skipped: boolean) => {
|
const skipPlugins = useCallback((skipped: boolean) => {
|
||||||
if (skipped === form.plugins.skipped) {
|
if (skipped === form.plugins.skipped) {
|
||||||
return;
|
return;
|
||||||
@@ -339,46 +269,6 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
});
|
});
|
||||||
}, [form]);
|
}, [form]);
|
||||||
|
|
||||||
const skipTeamMembers = useCallback((skipped: boolean) => {
|
|
||||||
if (skipped === form.teamMembers.skipped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setForm({
|
|
||||||
...form,
|
|
||||||
teamMembers: {
|
|
||||||
...form.teamMembers,
|
|
||||||
skipped,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [form]);
|
|
||||||
|
|
||||||
const getInviteMembersAnimationClass = useCallback(() => {
|
|
||||||
if (currentStep === WizardSteps.InviteMembers) {
|
|
||||||
return 'enter';
|
|
||||||
} else if (mostRecentStep === WizardSteps.InviteMembers) {
|
|
||||||
return 'exit';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}, [currentStep]);
|
|
||||||
|
|
||||||
let previous: React.ReactNode = (
|
|
||||||
<div
|
|
||||||
onClick={goPrevious}
|
|
||||||
onKeyUp={goPrevious}
|
|
||||||
tabIndex={0}
|
|
||||||
className='PreparingWorkspace__previous'
|
|
||||||
>
|
|
||||||
<i className='icon-chevron-up'/>
|
|
||||||
<FormattedMessage
|
|
||||||
id={'onboarding_wizard.previous'}
|
|
||||||
defaultMessage='Previous'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
if (currentStep === firstShowablePage) {
|
|
||||||
previous = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='PreparingWorkspace PreparingWorkspaceContainer'>
|
<div className='PreparingWorkspace PreparingWorkspaceContainer'>
|
||||||
{submissionState === SubmissionStates.SubmitFail && submitError && (
|
{submissionState === SubmissionStates.SubmitFail && submitError && (
|
||||||
@@ -401,49 +291,17 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
transitionSpeed={Animations.PAGE_SLIDE}
|
transitionSpeed={Animations.PAGE_SLIDE}
|
||||||
/>
|
/>
|
||||||
<div className='PreparingWorkspacePageContainer'>
|
<div className='PreparingWorkspacePageContainer'>
|
||||||
<Organization
|
|
||||||
onPageView={onPageViews[WizardSteps.Organization]}
|
|
||||||
show={shouldShowPage(WizardSteps.Organization)}
|
|
||||||
next={makeNext(WizardSteps.Organization)}
|
|
||||||
transitionDirection={getTransitionDirection(WizardSteps.Organization)}
|
|
||||||
organization={form.organization || ''}
|
|
||||||
setOrganization={(organization: Form['organization']) => {
|
|
||||||
setForm({
|
|
||||||
...form,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
setInviteId={(inviteId: string) => {
|
|
||||||
setForm({
|
|
||||||
...form,
|
|
||||||
teamMembers: {
|
|
||||||
...form.teamMembers,
|
|
||||||
inviteId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className='child-page'
|
|
||||||
createTeam={createTeam}
|
|
||||||
updateTeam={updateTeam}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Plugins
|
<Plugins
|
||||||
isSelfHosted={isSelfHosted}
|
|
||||||
onPageView={onPageViews[WizardSteps.Plugins]}
|
onPageView={onPageViews[WizardSteps.Plugins]}
|
||||||
previous={previous}
|
|
||||||
next={() => {
|
next={() => {
|
||||||
const pluginChoices = {...form.plugins};
|
const pluginChoices = {...form.plugins};
|
||||||
delete pluginChoices.skipped;
|
delete pluginChoices.skipped;
|
||||||
if (!isSelfHosted) {
|
|
||||||
setSubmissionState(SubmissionStates.UserRequested);
|
setSubmissionState(SubmissionStates.UserRequested);
|
||||||
}
|
|
||||||
makeNext(WizardSteps.Plugins)(pluginChoices);
|
makeNext(WizardSteps.Plugins)(pluginChoices);
|
||||||
skipPlugins(false);
|
skipPlugins(false);
|
||||||
}}
|
}}
|
||||||
skip={() => {
|
skip={() => {
|
||||||
if (!isSelfHosted) {
|
|
||||||
setSubmissionState(SubmissionStates.UserRequested);
|
setSubmissionState(SubmissionStates.UserRequested);
|
||||||
}
|
|
||||||
makeNext(WizardSteps.Plugins, true)();
|
makeNext(WizardSteps.Plugins, true)();
|
||||||
skipPlugins(true);
|
skipPlugins(true);
|
||||||
}}
|
}}
|
||||||
@@ -461,40 +319,12 @@ const PreparingWorkspace = (props: Props) => {
|
|||||||
transitionDirection={getTransitionDirection(WizardSteps.Plugins)}
|
transitionDirection={getTransitionDirection(WizardSteps.Plugins)}
|
||||||
className='child-page'
|
className='child-page'
|
||||||
/>
|
/>
|
||||||
<InviteMembers
|
|
||||||
onPageView={onPageViews[WizardSteps.InviteMembers]}
|
|
||||||
next={() => {
|
|
||||||
skipTeamMembers(false);
|
|
||||||
const inviteMembersTracking = {
|
|
||||||
inviteCount: form.teamMembers.invites.length,
|
|
||||||
};
|
|
||||||
setSubmissionState(SubmissionStates.UserRequested);
|
|
||||||
makeNext(WizardSteps.InviteMembers)(inviteMembersTracking);
|
|
||||||
}}
|
|
||||||
skip={() => {
|
|
||||||
skipTeamMembers(true);
|
|
||||||
setSubmissionState(SubmissionStates.UserRequested);
|
|
||||||
makeNext(WizardSteps.InviteMembers, true)();
|
|
||||||
}}
|
|
||||||
previous={previous}
|
|
||||||
show={shouldShowPage(WizardSteps.InviteMembers)}
|
|
||||||
transitionDirection={getTransitionDirection(WizardSteps.InviteMembers)}
|
|
||||||
disableEdits={submissionState !== SubmissionStates.Presubmit && submissionState !== SubmissionStates.SubmitFail}
|
|
||||||
className='child-page'
|
|
||||||
teamInviteId={team?.invite_id || form.teamMembers.inviteId}
|
|
||||||
configSiteUrl={configSiteUrl}
|
|
||||||
formUrl={form.url}
|
|
||||||
browserSiteUrl={browserSiteUrl}
|
|
||||||
/>
|
|
||||||
<LaunchingWorkspace
|
<LaunchingWorkspace
|
||||||
onPageView={onPageViews[WizardSteps.LaunchingWorkspace]}
|
onPageView={onPageViews[WizardSteps.LaunchingWorkspace]}
|
||||||
show={currentStep === WizardSteps.LaunchingWorkspace}
|
show={currentStep === WizardSteps.LaunchingWorkspace}
|
||||||
transitionDirection={getTransitionDirection(WizardSteps.LaunchingWorkspace)}
|
transitionDirection={getTransitionDirection(WizardSteps.LaunchingWorkspace)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`PreparingWorkspace__invite-members-illustration ${getInviteMembersAnimationClass()}`}>
|
|
||||||
<InviteMembersIllustration/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,5 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@
|
|||||||
import deepFreeze from 'mattermost-redux/utils/deep_freeze';
|
import deepFreeze from 'mattermost-redux/utils/deep_freeze';
|
||||||
|
|
||||||
export const WizardSteps = {
|
export const WizardSteps = {
|
||||||
Organization: 'Organization',
|
|
||||||
Plugins: 'Plugins',
|
Plugins: 'Plugins',
|
||||||
InviteMembers: 'InviteMembers',
|
|
||||||
LaunchingWorkspace: 'LaunchingWorkspace',
|
LaunchingWorkspace: 'LaunchingWorkspace',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -22,12 +20,8 @@ export const Animations = {
|
|||||||
|
|
||||||
export function mapStepToNextName(step: WizardStep): string {
|
export function mapStepToNextName(step: WizardStep): string {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case WizardSteps.Organization:
|
|
||||||
return 'admin_onboarding_next_organization';
|
|
||||||
case WizardSteps.Plugins:
|
case WizardSteps.Plugins:
|
||||||
return 'admin_onboarding_next_plugins';
|
return 'admin_onboarding_next_plugins';
|
||||||
case WizardSteps.InviteMembers:
|
|
||||||
return 'admin_onboarding_next_invite_members';
|
|
||||||
case WizardSteps.LaunchingWorkspace:
|
case WizardSteps.LaunchingWorkspace:
|
||||||
return 'admin_onboarding_next_transitioning_out';
|
return 'admin_onboarding_next_transitioning_out';
|
||||||
default:
|
default:
|
||||||
@@ -37,12 +31,8 @@ export function mapStepToNextName(step: WizardStep): string {
|
|||||||
|
|
||||||
export function mapStepToPrevious(step: WizardStep): string {
|
export function mapStepToPrevious(step: WizardStep): string {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case WizardSteps.Organization:
|
|
||||||
return 'admin_onboarding_previous_organization';
|
|
||||||
case WizardSteps.Plugins:
|
case WizardSteps.Plugins:
|
||||||
return 'admin_onboarding_previous_plugins';
|
return 'admin_onboarding_previous_plugins';
|
||||||
case WizardSteps.InviteMembers:
|
|
||||||
return 'admin_onboarding_previous_invite_members';
|
|
||||||
case WizardSteps.LaunchingWorkspace:
|
case WizardSteps.LaunchingWorkspace:
|
||||||
return 'admin_onboarding_previous_transitioning_out';
|
return 'admin_onboarding_previous_transitioning_out';
|
||||||
default:
|
default:
|
||||||
@@ -52,12 +42,8 @@ export function mapStepToPrevious(step: WizardStep): string {
|
|||||||
|
|
||||||
export function mapStepToPageView(step: WizardStep): string {
|
export function mapStepToPageView(step: WizardStep): string {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case WizardSteps.Organization:
|
|
||||||
return 'pageview_admin_onboarding_organization';
|
|
||||||
case WizardSteps.Plugins:
|
case WizardSteps.Plugins:
|
||||||
return 'pageview_admin_onboarding_plugins';
|
return 'pageview_admin_onboarding_plugins';
|
||||||
case WizardSteps.InviteMembers:
|
|
||||||
return 'pageview_admin_onboarding_invite_members';
|
|
||||||
case WizardSteps.LaunchingWorkspace:
|
case WizardSteps.LaunchingWorkspace:
|
||||||
return 'pageview_admin_onboarding_transitioning_out';
|
return 'pageview_admin_onboarding_transitioning_out';
|
||||||
default:
|
default:
|
||||||
@@ -67,12 +53,8 @@ export function mapStepToPageView(step: WizardStep): string {
|
|||||||
|
|
||||||
export function mapStepToSubmitFail(step: WizardStep): string {
|
export function mapStepToSubmitFail(step: WizardStep): string {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case WizardSteps.Organization:
|
|
||||||
return 'admin_onboarding_organization_submit_fail';
|
|
||||||
case WizardSteps.Plugins:
|
case WizardSteps.Plugins:
|
||||||
return 'admin_onboarding_plugins_submit_fail';
|
return 'admin_onboarding_plugins_submit_fail';
|
||||||
case WizardSteps.InviteMembers:
|
|
||||||
return 'admin_onboarding_invite_members_submit_fail';
|
|
||||||
case WizardSteps.LaunchingWorkspace:
|
case WizardSteps.LaunchingWorkspace:
|
||||||
return 'admin_onboarding_transitioning_out_submit_fail';
|
return 'admin_onboarding_transitioning_out_submit_fail';
|
||||||
default:
|
default:
|
||||||
@@ -82,12 +64,8 @@ export function mapStepToSubmitFail(step: WizardStep): string {
|
|||||||
|
|
||||||
export function mapStepToSkipName(step: WizardStep): string {
|
export function mapStepToSkipName(step: WizardStep): string {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case WizardSteps.Organization:
|
|
||||||
return 'admin_onboarding_skip_organization';
|
|
||||||
case WizardSteps.Plugins:
|
case WizardSteps.Plugins:
|
||||||
return 'admin_onboarding_skip_plugins';
|
return 'admin_onboarding_skip_plugins';
|
||||||
case WizardSteps.InviteMembers:
|
|
||||||
return 'admin_onboarding_skip_invite_members';
|
|
||||||
case WizardSteps.LaunchingWorkspace:
|
case WizardSteps.LaunchingWorkspace:
|
||||||
return 'admin_onboarding_skip_transitioning_out';
|
return 'admin_onboarding_skip_transitioning_out';
|
||||||
default:
|
default:
|
||||||
@@ -150,14 +128,12 @@ export type Form = {
|
|||||||
skipped: boolean;
|
skipped: boolean;
|
||||||
};
|
};
|
||||||
teamMembers: {
|
teamMembers: {
|
||||||
inviteId: string;
|
|
||||||
invites: string[];
|
invites: string[];
|
||||||
skipped: boolean;
|
skipped: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const emptyForm = deepFreeze({
|
export const emptyForm = deepFreeze({
|
||||||
organization: '',
|
|
||||||
inferredProtocol: null,
|
inferredProtocol: null,
|
||||||
urlSkipped: false,
|
urlSkipped: false,
|
||||||
useCase: {
|
useCase: {
|
||||||
@@ -180,7 +156,6 @@ export const emptyForm = deepFreeze({
|
|||||||
skipped: false,
|
skipped: false,
|
||||||
},
|
},
|
||||||
teamMembers: {
|
teamMembers: {
|
||||||
inviteId: '',
|
|
||||||
invites: [],
|
invites: [],
|
||||||
skipped: false,
|
skipped: false,
|
||||||
},
|
},
|
||||||
@@ -190,7 +165,7 @@ export type PreparingWorkspacePageProps = {
|
|||||||
transitionDirection: AnimationReason;
|
transitionDirection: AnimationReason;
|
||||||
next?: () => void;
|
next?: () => void;
|
||||||
skip?: () => void;
|
skip?: () => void;
|
||||||
previous?: React.ReactNode;
|
previous?: JSX.Element;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
onPageView: () => void;
|
onPageView: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import classNames from 'classnames';
|
|||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder';
|
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder';
|
||||||
import {General} from 'mattermost-redux/constants';
|
import {General} from 'mattermost-redux/constants';
|
||||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
import {Theme, getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getCurrentUser, isCurrentUserSystemAdmin, checkIsFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser, isCurrentUserSystemAdmin, checkIsFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {setUrl} from 'mattermost-redux/actions/general';
|
import {setUrl} from 'mattermost-redux/actions/general';
|
||||||
@@ -89,8 +89,6 @@ import {ActionResult} from 'mattermost-redux/types/actions';
|
|||||||
|
|
||||||
import WelcomePostRenderer from 'components/welcome_post_renderer';
|
import WelcomePostRenderer from 'components/welcome_post_renderer';
|
||||||
|
|
||||||
import {getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
|
||||||
|
|
||||||
import {applyLuxonDefaults} from './effects';
|
import {applyLuxonDefaults} from './effects';
|
||||||
|
|
||||||
import RootProvider from './root_provider';
|
import RootProvider from './root_provider';
|
||||||
@@ -360,8 +358,8 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const myTeams = getMyTeams(storeState);
|
const useCaseOnboarding = getUseCaseOnboarding(storeState);
|
||||||
if (myTeams.length > 0) {
|
if (!useCaseOnboarding) {
|
||||||
GlobalActions.redirectUserToDefaultTeam();
|
GlobalActions.redirectUserToDefaultTeam();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {connect} from 'react-redux';
|
|||||||
|
|
||||||
import {getFirstAdminSetupComplete} from 'mattermost-redux/actions/general';
|
import {getFirstAdminSetupComplete} from 'mattermost-redux/actions/general';
|
||||||
import {getCurrentUserId, isCurrentUserSystemAdmin, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId, isCurrentUserSystemAdmin, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
@@ -13,7 +14,11 @@ import {GlobalState} from 'types/store';
|
|||||||
import RootRedirect, {Props} from './root_redirect';
|
import RootRedirect, {Props} from './root_redirect';
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
const isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state);
|
const useCaseOnboarding = getUseCaseOnboarding(state);
|
||||||
|
let isElegibleForFirstAdmingOnboarding = useCaseOnboarding;
|
||||||
|
if (isElegibleForFirstAdmingOnboarding) {
|
||||||
|
isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
currentUserId: getCurrentUserId(state),
|
currentUserId: getCurrentUserId(state),
|
||||||
isElegibleForFirstAdmingOnboarding,
|
isElegibleForFirstAdmingOnboarding,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {IntlProvider} from 'react-intl';
|
|||||||
import {BrowserRouter} from 'react-router-dom';
|
import {BrowserRouter} from 'react-router-dom';
|
||||||
import {act, screen} from '@testing-library/react';
|
import {act, screen} from '@testing-library/react';
|
||||||
|
|
||||||
|
import * as global_actions from 'actions/global_actions';
|
||||||
|
|
||||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
|
|
||||||
import Signup from 'components/signup/signup';
|
import Signup from 'components/signup/signup';
|
||||||
@@ -195,6 +197,9 @@ describe('components/signup/Signup', () => {
|
|||||||
mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser
|
mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser
|
||||||
mockResolvedValueOnce({error: {server_error_id: 'api.user.login.not_verified.app_error'}}); // loginById
|
mockResolvedValueOnce({error: {server_error_id: 'api.user.login.not_verified.app_error'}}); // loginById
|
||||||
|
|
||||||
|
const mockRedirectUserToDefaultTeam = jest.fn();
|
||||||
|
jest.spyOn(global_actions, 'redirectUserToDefaultTeam').mockImplementation(mockRedirectUserToDefaultTeam);
|
||||||
|
|
||||||
const wrapper = mountWithIntl(
|
const wrapper = mountWithIntl(
|
||||||
<IntlProvider {...intlProviderProps}>
|
<IntlProvider {...intlProviderProps}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -223,6 +228,7 @@ describe('components/signup/Signup', () => {
|
|||||||
expect(wrapper.find('#input_name').first().props().disabled).toEqual(true);
|
expect(wrapper.find('#input_name').first().props().disabled).toEqual(true);
|
||||||
expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true);
|
expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true);
|
||||||
|
|
||||||
|
expect(mockRedirectUserToDefaultTeam).not.toHaveBeenCalled();
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith('/should_verify_email?email=jdoe%40mm.com&teamname=teamName');
|
expect(mockHistoryPush).toHaveBeenCalledWith('/should_verify_email?email=jdoe%40mm.com&teamname=teamName');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,6 +238,9 @@ describe('components/signup/Signup', () => {
|
|||||||
mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser
|
mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser
|
||||||
mockResolvedValueOnce({}); // loginById
|
mockResolvedValueOnce({}); // loginById
|
||||||
|
|
||||||
|
const mockRedirectUserToDefaultTeam = jest.fn();
|
||||||
|
jest.spyOn(global_actions, 'redirectUserToDefaultTeam').mockImplementation(mockRedirectUserToDefaultTeam);
|
||||||
|
|
||||||
const wrapper = mountWithIntl(
|
const wrapper = mountWithIntl(
|
||||||
<IntlProvider {...intlProviderProps}>
|
<IntlProvider {...intlProviderProps}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -259,6 +268,8 @@ describe('components/signup/Signup', () => {
|
|||||||
expect(wrapper.find(Input).first().props().disabled).toEqual(true);
|
expect(wrapper.find(Input).first().props().disabled).toEqual(true);
|
||||||
expect(wrapper.find('#input_name').first().props().disabled).toEqual(true);
|
expect(wrapper.find('#input_name').first().props().disabled).toEqual(true);
|
||||||
expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true);
|
expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true);
|
||||||
|
|
||||||
|
expect(mockRedirectUserToDefaultTeam).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add user to team and redirect when team invite valid and logged in', async () => {
|
it('should add user to team and redirect when team invite valid and logged in', async () => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {getTeamInviteInfo} from 'mattermost-redux/actions/teams';
|
|||||||
import {createUser, loadMe, loadMeREST} from 'mattermost-redux/actions/users';
|
import {createUser, loadMe, loadMeREST} from 'mattermost-redux/actions/users';
|
||||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ import {GlobalState} from 'types/store';
|
|||||||
|
|
||||||
import {getGlobalItem} from 'selectors/storage';
|
import {getGlobalItem} from 'selectors/storage';
|
||||||
|
|
||||||
|
import {redirectUserToDefaultTeam} from 'actions/global_actions';
|
||||||
import {removeGlobalItem, setGlobalItem} from 'actions/storage';
|
import {removeGlobalItem, setGlobalItem} from 'actions/storage';
|
||||||
import {addUserToTeamFromInvite} from 'actions/team_actions';
|
import {addUserToTeamFromInvite} from 'actions/team_actions';
|
||||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||||
@@ -103,6 +104,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
|||||||
} = config;
|
} = config;
|
||||||
const {IsLicensed, Cloud} = useSelector(getLicense);
|
const {IsLicensed, Cloud} = useSelector(getLicense);
|
||||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||||
|
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||||
const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined));
|
const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined));
|
||||||
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
||||||
|
|
||||||
@@ -308,7 +310,15 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
|||||||
} else if (inviteId) {
|
} else if (inviteId) {
|
||||||
getInviteInfo(inviteId);
|
getInviteInfo(inviteId);
|
||||||
} else if (loggedIn) {
|
} else if (loggedIn) {
|
||||||
|
if (useCaseOnboarding) {
|
||||||
|
// need info about whether admin or not,
|
||||||
|
// and whether admin has already completed
|
||||||
|
// first tiem onboarding. Instead of fetching and orchestrating that here,
|
||||||
|
// let the default root component handle it.
|
||||||
history.push('/');
|
history.push('/');
|
||||||
|
} else {
|
||||||
|
redirectUserToDefaultTeam();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,12 +461,14 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
|||||||
|
|
||||||
if (redirectTo) {
|
if (redirectTo) {
|
||||||
history.push(redirectTo);
|
history.push(redirectTo);
|
||||||
} else {
|
} else if (useCaseOnboarding) {
|
||||||
// need info about whether admin or not,
|
// need info about whether admin or not,
|
||||||
// and whether admin has already completed
|
// and whether admin has already completed
|
||||||
// first tiem onboarding. Instead of fetching and orchestrating that here,
|
// first tiem onboarding. Instead of fetching and orchestrating that here,
|
||||||
// let the default root component handle it.
|
// let the default root component handle it.
|
||||||
history.push('/');
|
history.push('/');
|
||||||
|
} else {
|
||||||
|
redirectUserToDefaultTeam();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
|||||||
|
|
||||||
import {getTermsOfService, updateMyTermsOfServiceStatus} from 'mattermost-redux/actions/users';
|
import {getTermsOfService, updateMyTermsOfServiceStatus} from 'mattermost-redux/actions/users';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
import {GlobalState} from '@mattermost/types/store';
|
import {GlobalState} from '@mattermost/types/store';
|
||||||
import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
|
import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
|
||||||
@@ -25,7 +26,9 @@ type Actions = {
|
|||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
|
const useCaseOnboarding = getUseCaseOnboarding(state);
|
||||||
return {
|
return {
|
||||||
|
useCaseOnboarding,
|
||||||
termsEnabled: config.EnableCustomTermsOfService === 'true',
|
termsEnabled: config.EnableCustomTermsOfService === 'true',
|
||||||
emojiMap: getEmojiMap(state),
|
emojiMap: getEmojiMap(state),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ describe('components/terms_of_service/TermsOfService', () => {
|
|||||||
location: {search: ''},
|
location: {search: ''},
|
||||||
termsEnabled: true,
|
termsEnabled: true,
|
||||||
emojiMap: {} as EmojiMap,
|
emojiMap: {} as EmojiMap,
|
||||||
|
useCaseOnboarding: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot', () => {
|
test('should match snapshot', () => {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface TermsOfServiceProps {
|
|||||||
) => {data: UpdateMyTermsOfServiceStatusResponse};
|
) => {data: UpdateMyTermsOfServiceStatusResponse};
|
||||||
};
|
};
|
||||||
emojiMap: EmojiMap;
|
emojiMap: EmojiMap;
|
||||||
|
useCaseOnboarding: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TermsOfServiceState {
|
interface TermsOfServiceState {
|
||||||
@@ -110,12 +111,14 @@ export default class TermsOfService extends React.PureComponent<TermsOfServicePr
|
|||||||
const redirectTo = query.get('redirect_to');
|
const redirectTo = query.get('redirect_to');
|
||||||
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
||||||
getHistory().push(redirectTo);
|
getHistory().push(redirectTo);
|
||||||
} else {
|
} else if (this.props.useCaseOnboarding) {
|
||||||
// need info about whether admin or not,
|
// need info about whether admin or not,
|
||||||
// and whether admin has already completed
|
// and whether admin has already completed
|
||||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||||
// let the default root component handle it.
|
// let the default root component handle it.
|
||||||
getHistory().push('/');
|
getHistory().push('/');
|
||||||
|
} else {
|
||||||
|
GlobalActions.redirectUserToDefaultTeam();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -165,8 +165,7 @@ const StyledCustomized = styled(Customize)`
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid rgba(var(--center-channel-text-rgb), 0.16);
|
border: 1px solid rgba(var(--center-channel-text-rgb), 0.16);
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 1px solid var(--button-bg);
|
border: 2px solid var(--button-bg);
|
||||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
|||||||
|
|
||||||
const [integrations, setIntegrations] = useState<Integration[]>();
|
const [integrations, setIntegrations] = useState<Integration[]>();
|
||||||
|
|
||||||
const marketplacePlugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins);
|
const plugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins);
|
||||||
const loadedPlugins = useSelector((state: GlobalState) => state.plugins.plugins);
|
|
||||||
|
|
||||||
const [illustrationDetails, setIllustrationDetails] = useState<IllustrationAnimations>(() => {
|
const [illustrationDetails, setIllustrationDetails] = useState<IllustrationAnimations>(() => {
|
||||||
const defaultIllustration = getTemplateDefaultIllustration(template);
|
const defaultIllustration = getTemplateDefaultIllustration(template);
|
||||||
@@ -131,14 +130,13 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
|||||||
const intg =
|
const intg =
|
||||||
availableIntegrations?.
|
availableIntegrations?.
|
||||||
flatMap((integration) => {
|
flatMap((integration) => {
|
||||||
return marketplacePlugins.reduce((acc: Integration[], curr) => {
|
return plugins.reduce((acc: Integration[], curr) => {
|
||||||
if (curr.manifest.id === integration.id) {
|
if (curr.manifest.id === integration.id) {
|
||||||
const installed = Boolean(loadedPlugins[integration.id]);
|
|
||||||
acc.push({
|
acc.push({
|
||||||
...integration,
|
...integration,
|
||||||
name: curr.manifest.name,
|
name: curr.manifest.name,
|
||||||
icon: curr.icon_data,
|
icon: curr.icon_data,
|
||||||
installed,
|
installed: curr.installed_version !== '',
|
||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
@@ -151,7 +149,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
|||||||
if (intg?.length) {
|
if (intg?.length) {
|
||||||
setIntegrations(intg);
|
setIntegrations(intg);
|
||||||
}
|
}
|
||||||
}, [marketplacePlugins, availableIntegrations, loadedPlugins, pluginsEnabled]);
|
}, [plugins, availableIntegrations, pluginsEnabled]);
|
||||||
|
|
||||||
// building accordion items
|
// building accordion items
|
||||||
const accordionItemsData: AccordionItemType[] = [];
|
const accordionItemsData: AccordionItemType[] = [];
|
||||||
@@ -206,7 +204,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
|||||||
)],
|
)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (pluginsEnabled && integrations?.length) {
|
if (integrations?.length && pluginsEnabled) {
|
||||||
accordionItemsData.push({
|
accordionItemsData.push({
|
||||||
id: 'integrations',
|
id: 'integrations',
|
||||||
icon: <i className='icon-power-plug-outline'/>,
|
icon: <i className='icon-power-plug-outline'/>,
|
||||||
@@ -305,7 +303,6 @@ const StyledPreview = styled(Preview)`
|
|||||||
width: 387px;
|
width: 387px;
|
||||||
height: 416px;
|
height: 416px;
|
||||||
padding-right: 32px;
|
padding-right: 32px;
|
||||||
margin-top: 17px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const Accordion = styled(LibAccordion)`
|
|||||||
.accordion-card {
|
.accordion-card {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid transparent;
|
|
||||||
color: var(--center-channel-color);
|
color: var(--center-channel-color);
|
||||||
|
|
||||||
.accordion-card-header {
|
.accordion-card-header {
|
||||||
@@ -47,7 +46,7 @@ const Accordion = styled(LibAccordion)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-color: var(--denim-button-bg);
|
border: 1px solid var(--denim-button-bg);
|
||||||
|
|
||||||
.accordion-card-header {
|
.accordion-card-header {
|
||||||
color: var(--denim-button-bg);
|
color: var(--denim-button-bg);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {ReactNode, useCallback, useEffect, useState} from 'react';
|
import React, {ReactNode, useEffect, useState} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -110,28 +110,6 @@ const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps
|
|||||||
id: 'work_templates.preview.integrations.admin_install.notify',
|
id: 'work_templates.preview.integrations.admin_install.notify',
|
||||||
defaultMessage: 'Notify admin to install integrations.',
|
defaultMessage: 'Notify admin to install integrations.',
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeIntegrationSubtext = useCallback((integration: IntegrationPreviewSectionItemsProps) => {
|
|
||||||
if (integration.installed) {
|
|
||||||
return formatMessage({
|
|
||||||
id: 'work_templates.preview.integrations.already_installed',
|
|
||||||
defaultMessage: 'Already installed',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pluginInstallationPossible) {
|
|
||||||
return formatMessage({
|
|
||||||
id: 'work_templates.preview.integrations.app_install',
|
|
||||||
defaultMessage: 'App Install',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatMessage({
|
|
||||||
id: 'work_templates.preview.integrations.to_be_installed',
|
|
||||||
defaultMessage: 'To be installed',
|
|
||||||
});
|
|
||||||
}, [pluginInstallationPossible, formatMessage]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='preview-integrations'>
|
<div className='preview-integrations'>
|
||||||
<div className='preview-integrations-plugins'>
|
<div className='preview-integrations-plugins'>
|
||||||
@@ -141,17 +119,15 @@ const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
className={classnames('preview-integrations-plugins-item', {'preview-integrations-plugins-item__readonly': !item.installed && !pluginInstallationPossible})}
|
className={classnames('preview-integrations-plugins-item', {'preview-integrations-plugins-item__readonly': !item.installed && !pluginInstallationPossible})}
|
||||||
>
|
>
|
||||||
<div className='preview-integrations-plugins-item__illustration'>
|
<div className='preview-integrations-plugins-item__icon'>
|
||||||
<img src={item.icon}/>
|
<img src={item.icon}/>
|
||||||
</div>
|
</div>
|
||||||
<div className='preview-integrations-plugins-item__name'>
|
<div className='preview-integrations-plugins-item__name'>
|
||||||
{item.name}<br/>
|
{item.name}
|
||||||
<span className='preview-integrations-plugins-item__name-sub'>
|
|
||||||
{makeIntegrationSubtext(item)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{item.installed &&
|
{item.installed &&
|
||||||
<div className='preview-integrations-plugins-item__icon icon-check-circle preview-integrations-plugins-item__icon_blue'/>}
|
<div className='icon-check-circle preview-integrations-plugins-item__icon_blue'/>}
|
||||||
|
{!item.installed && <div className='icon-download-outline'/>}
|
||||||
</div>);
|
</div>);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +205,6 @@ const StyledPreviewSection = styled(PreviewSection)`
|
|||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
width: 128px;
|
width: 128px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
flex-basis: 45%;
|
flex-basis: 45%;
|
||||||
@@ -240,7 +215,7 @@ const StyledPreviewSection = styled(PreviewSection)`
|
|||||||
opacity: 65%;
|
opacity: 65%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__illustration {
|
&__icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@@ -252,30 +227,22 @@ const StyledPreviewSection = styled(PreviewSection)`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_blue {
|
||||||
|
color: var(--denim-button-bg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
flex-grow: 2;
|
flex-grow: 2;
|
||||||
|
margin-top: 8px;
|
||||||
color: var(--center-channel-text);
|
color: var(--center-channel-text);
|
||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
line-height: 16px;
|
line-height: 22px;
|
||||||
&-sub {
|
|
||||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
align-self: flex-start;
|
|
||||||
|
|
||||||
&_blue {
|
|
||||||
color: var(--denim-button-bg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,8 +264,8 @@ const StyledPreviewSection = styled(PreviewSection)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-check-circle::before {
|
.icon-check-circle::before {
|
||||||
margin-top: 2px;
|
margin-top: 8px;
|
||||||
margin-right: 2px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-download-outline::before {
|
.icon-download-outline::before {
|
||||||
|
|||||||
@@ -4318,26 +4318,10 @@
|
|||||||
"notify_here.question": "By using **@here** you are about to send notifications to up to **{totalMembers} other people**. Are you sure you want to do this?",
|
"notify_here.question": "By using **@here** you are about to send notifications to up to **{totalMembers} other people**. Are you sure you want to do this?",
|
||||||
"notify_here.question_timezone": "By using **@here** you are about to send notifications to up to **{totalMembers} other people** in **{timezones, number} {timezones, plural, one {timezone} other {timezones}}**. Are you sure you want to do this?",
|
"notify_here.question_timezone": "By using **@here** you are about to send notifications to up to **{totalMembers} other people** in **{timezones, number} {timezones, plural, one {timezone} other {timezones}}**. Are you sure you want to do this?",
|
||||||
"numMembers": "{num, number} {num, plural, one {member} other {members}}",
|
"numMembers": "{num, number} {num, plural, one {member} other {members}}",
|
||||||
"onboarding_wizard.cloud_plugins.description": "Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we'll add them to your workspace. Additional set up may be needed later.",
|
|
||||||
"onboarding_wizard.cloud_plugins.subtitle": "(almost there!)",
|
|
||||||
"onboarding_wizard.cloud_plugins.title": "Welcome to Mattermost!",
|
|
||||||
"onboarding_wizard.invite_members.copied_link": "Link Copied",
|
|
||||||
"onboarding_wizard.invite_members.copy_link": "Copy Link",
|
|
||||||
"onboarding_wizard.invite_members.copy_link_input": "team invite link",
|
|
||||||
"onboarding_wizard.invite_members.description_link": "Collaboration is tough by yourself. Invite a few team members using the invitation link below.",
|
|
||||||
"onboarding_wizard.invite_members.next_link": "Finish setup",
|
|
||||||
"onboarding_wizard.invite_members.title": "Invite your team members",
|
|
||||||
"onboarding_wizard.launching_workspace.description": "It’ll be ready in a moment",
|
"onboarding_wizard.launching_workspace.description": "It’ll be ready in a moment",
|
||||||
"onboarding_wizard.launching_workspace.title": "Launching your workspace now",
|
"onboarding_wizard.launching_workspace.title": "Launching your workspace now",
|
||||||
"onboarding_wizard.next": "Continue",
|
"onboarding_wizard.next": "Continue",
|
||||||
"onboarding_wizard.organization.description": "We’ll use this to help personalize your workspace.",
|
"onboarding_wizard.plugins.description": "Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we'll add them to your workspace. Additional set up may be needed later.",
|
||||||
"onboarding_wizard.organization.empty": "You must enter an organization name",
|
|
||||||
"onboarding_wizard.organization.length": "Organization name must be between {min} and {max} characters",
|
|
||||||
"onboarding_wizard.organization.other": "Invalid organization name: {reason}",
|
|
||||||
"onboarding_wizard.organization.placeholder": "Organization name",
|
|
||||||
"onboarding_wizard.organization.reserved": "Organization name may not <a>start with a reserved word</a>.",
|
|
||||||
"onboarding_wizard.organization.team_api_error": "There was an error, please try again.",
|
|
||||||
"onboarding_wizard.organization.title": "What’s the name of your organization?",
|
|
||||||
"onboarding_wizard.plugins.github": "GitHub",
|
"onboarding_wizard.plugins.github": "GitHub",
|
||||||
"onboarding_wizard.plugins.github.tooltip": "Subscribe to repositories, stay up to date with reviews, assignments",
|
"onboarding_wizard.plugins.github.tooltip": "Subscribe to repositories, stay up to date with reviews, assignments",
|
||||||
"onboarding_wizard.plugins.gitlab": "GitLab",
|
"onboarding_wizard.plugins.gitlab": "GitLab",
|
||||||
@@ -4345,14 +4329,13 @@
|
|||||||
"onboarding_wizard.plugins.jira": "Jira",
|
"onboarding_wizard.plugins.jira": "Jira",
|
||||||
"onboarding_wizard.plugins.jira.tooltip": "Create Jira tickets from messages in Mattermost, get notified of important updates in Jira",
|
"onboarding_wizard.plugins.jira.tooltip": "Create Jira tickets from messages in Mattermost, get notified of important updates in Jira",
|
||||||
"onboarding_wizard.plugins.marketplace": "More tools can be added once your workspace is set up. To see all available integrations, <a>visit the Marketplace.</a>",
|
"onboarding_wizard.plugins.marketplace": "More tools can be added once your workspace is set up. To see all available integrations, <a>visit the Marketplace.</a>",
|
||||||
|
"onboarding_wizard.plugins.subtitle": "(almost there!)",
|
||||||
|
"onboarding_wizard.plugins.title": "Welcome to Mattermost!",
|
||||||
"onboarding_wizard.plugins.todo": "To do",
|
"onboarding_wizard.plugins.todo": "To do",
|
||||||
"onboarding_wizard.plugins.todo.tooltip": "A plugin to track Todo issues in a list and send you daily reminders about your Todo list",
|
"onboarding_wizard.plugins.todo.tooltip": "A plugin to track Todo issues in a list and send you daily reminders about your Todo list",
|
||||||
"onboarding_wizard.plugins.zoom": "Zoom",
|
"onboarding_wizard.plugins.zoom": "Zoom",
|
||||||
"onboarding_wizard.plugins.zoom.tooltip": "Start Zoom audio and video conferencing calls in Mattermost with a single click",
|
"onboarding_wizard.plugins.zoom.tooltip": "Start Zoom audio and video conferencing calls in Mattermost with a single click",
|
||||||
"onboarding_wizard.previous": "Previous",
|
"onboarding_wizard.skip": "Skip for now",
|
||||||
"onboarding_wizard.self_hosted_plugins.description": "Choose the tools you work with, and we'll add them to your workspace. Additional set up may be needed later.",
|
|
||||||
"onboarding_wizard.self_hosted_plugins.title": "What tools do you use?",
|
|
||||||
"onboarding_wizard.skip-button": "Skip",
|
|
||||||
"onboarding_wizard.submit_error.generic": "Something went wrong. Please try again.",
|
"onboarding_wizard.submit_error.generic": "Something went wrong. Please try again.",
|
||||||
"onboardingTask.checklist.completed_subtitle": "We hope Mattermost is more familiar now.",
|
"onboardingTask.checklist.completed_subtitle": "We hope Mattermost is more familiar now.",
|
||||||
"onboardingTask.checklist.completed_title": "Well done. You’ve completed all of the tasks!",
|
"onboardingTask.checklist.completed_title": "Well done. You’ve completed all of the tasks!",
|
||||||
@@ -5784,9 +5767,6 @@
|
|||||||
"work_templates.preview.integrations.admin_install.multiple_plugin": "Integrations will not be added until admin installs them.",
|
"work_templates.preview.integrations.admin_install.multiple_plugin": "Integrations will not be added until admin installs them.",
|
||||||
"work_templates.preview.integrations.admin_install.notify": "Notify admin to install integrations",
|
"work_templates.preview.integrations.admin_install.notify": "Notify admin to install integrations",
|
||||||
"work_templates.preview.integrations.admin_install.single_plugin": "{plugin} will not be added until admin installs it.",
|
"work_templates.preview.integrations.admin_install.single_plugin": "{plugin} will not be added until admin installs it.",
|
||||||
"work_templates.preview.integrations.already_installed": "Already installed",
|
|
||||||
"work_templates.preview.integrations.app_install": "App Install",
|
|
||||||
"work_templates.preview.integrations.to_be_installed": "To be installed",
|
|
||||||
"work_templates.preview.modal_cancel_button": "Back",
|
"work_templates.preview.modal_cancel_button": "Back",
|
||||||
"work_templates.preview.modal_next_button": "Next",
|
"work_templates.preview.modal_next_button": "Next",
|
||||||
"work_templates.preview.modal_title": "Preview {useCase}",
|
"work_templates.preview.modal_title": "Preview {useCase}",
|
||||||
|
|||||||
@@ -4,18 +4,6 @@
|
|||||||
import {AnyAction} from 'redux';
|
import {AnyAction} from 'redux';
|
||||||
import {batchActions} from 'redux-batched-actions';
|
import {batchActions} from 'redux-batched-actions';
|
||||||
|
|
||||||
import {ServerError} from '@mattermost/types/errors';
|
|
||||||
import {
|
|
||||||
Channel,
|
|
||||||
ChannelNotifyProps,
|
|
||||||
ChannelMembership,
|
|
||||||
ChannelModerationPatch,
|
|
||||||
ChannelsWithTotalCount,
|
|
||||||
ChannelSearchOpts,
|
|
||||||
ServerChannel,
|
|
||||||
} from '@mattermost/types/channels';
|
|
||||||
import {PreferenceType} from '@mattermost/types/preferences';
|
|
||||||
|
|
||||||
import {ChannelTypes, PreferenceTypes, UserTypes} from 'mattermost-redux/action_types';
|
import {ChannelTypes, PreferenceTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
@@ -31,12 +19,18 @@ import {
|
|||||||
getRedirectChannelNameForTeam,
|
getRedirectChannelNameForTeam,
|
||||||
isManuallyUnread,
|
isManuallyUnread,
|
||||||
} from 'mattermost-redux/selectors/entities/channels';
|
} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getServerVersion} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
import {ActionFunc, ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
|
import {ActionFunc, ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {getChannelByName} from 'mattermost-redux/utils/channel_utils';
|
import {getChannelsIdForTeam, getChannelByName} from 'mattermost-redux/utils/channel_utils';
|
||||||
|
|
||||||
|
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||||
|
|
||||||
|
import {Channel, ChannelNotifyProps, ChannelMembership, ChannelModerationPatch, ChannelsWithTotalCount, ChannelSearchOpts} from '@mattermost/types/channels';
|
||||||
|
|
||||||
|
import {PreferenceType} from '@mattermost/types/preferences';
|
||||||
|
|
||||||
import {General, Preferences} from '../constants';
|
import {General, Preferences} from '../constants';
|
||||||
|
|
||||||
@@ -468,33 +462,52 @@ export function getChannelTimezones(channelId: string): ActionFunc {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc<{channels: ServerChannel[]; channelMembers: ChannelMembership[]}> {
|
export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
|
dispatch({
|
||||||
|
type: ChannelTypes.CHANNELS_REQUEST,
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
|
||||||
let channels;
|
let channels;
|
||||||
let channelMembers;
|
let channelMembers;
|
||||||
|
const state = getState();
|
||||||
|
const shouldFetchArchived = isMinimumServerVersion(getServerVersion(state), 5, 21);
|
||||||
try {
|
try {
|
||||||
[channels, channelMembers] = await Promise.all([
|
[channels, channelMembers] = await Promise.all([
|
||||||
Client4.getMyChannels(teamId),
|
Client4.getMyChannels(teamId, shouldFetchArchived),
|
||||||
Client4.getMyChannelMembers(teamId),
|
Client4.getMyChannelMembers(teamId),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
|
dispatch({type: ChannelTypes.CHANNELS_FAILURE, error});
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
return {error: error as ServerError};
|
return {error};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {currentUserId} = state.entities.users;
|
||||||
|
const {currentChannelId} = state.entities.channels;
|
||||||
|
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
{
|
{
|
||||||
type: ChannelTypes.RECEIVED_CHANNELS,
|
type: ChannelTypes.RECEIVED_CHANNELS,
|
||||||
teamId,
|
teamId,
|
||||||
data: channels,
|
data: channels,
|
||||||
|
currentChannelId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ChannelTypes.CHANNELS_SUCCESS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
|
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
|
||||||
data: channelMembers,
|
data: channelMembers,
|
||||||
|
sync: !shouldFetchArchived,
|
||||||
|
channels,
|
||||||
|
remove: getChannelsIdForTeam(state, teamId),
|
||||||
|
currentUserId,
|
||||||
|
currentChannelId,
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
|
|
||||||
const roles = new Set<string>();
|
const roles = new Set<string>();
|
||||||
for (const member of channelMembers) {
|
for (const member of channelMembers) {
|
||||||
for (const role of member.roles.split(' ')) {
|
for (const role of member.roles.split(' ')) {
|
||||||
@@ -505,7 +518,7 @@ export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc<{chann
|
|||||||
dispatch(loadRolesIfNeeded(roles));
|
dispatch(loadRolesIfNeeded(roles));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {data: {channels, channelMembers}};
|
return {data: {channels, members: channelMembers}};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,6 +245,10 @@ export function isCustomGroupsEnabled(state: GlobalState): boolean {
|
|||||||
return getConfig(state).EnableCustomGroups === 'true';
|
return getConfig(state).EnableCustomGroups === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUseCaseOnboarding(state: GlobalState): boolean {
|
||||||
|
return getFeatureFlagValue(state, 'UseCaseOnboarding') === 'true' && getLicense(state)?.Cloud === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
export function insightsAreEnabled(state: GlobalState): boolean {
|
export function insightsAreEnabled(state: GlobalState): boolean {
|
||||||
const isConfiguredForFeature = getConfig(state).InsightsEnabled === 'true';
|
const isConfiguredForFeature = getConfig(state).InsightsEnabled === 'true';
|
||||||
const featureIsEnabled = getFeatureFlagValue(state, 'InsightsEnabled') === 'true';
|
const featureIsEnabled = getFeatureFlagValue(state, 'InsightsEnabled') === 'true';
|
||||||
|
|||||||
@@ -1099,7 +1099,6 @@ export const DocLinks = {
|
|||||||
ONBOARD_LDAP: 'https://docs.mattermost.com/onboard/ad-ldap.html',
|
ONBOARD_LDAP: 'https://docs.mattermost.com/onboard/ad-ldap.html',
|
||||||
ONBOARD_SSO: 'https://docs.mattermost.com/onboard/sso-saml.html',
|
ONBOARD_SSO: 'https://docs.mattermost.com/onboard/sso-saml.html',
|
||||||
TRUE_UP_REVIEW: 'https://mattermost.com/pl/true-up-documentation',
|
TRUE_UP_REVIEW: 'https://mattermost.com/pl/true-up-documentation',
|
||||||
ABOUT_TEAMS: 'https://docs.mattermost.com/welcome/about-teams.html#team-url',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LicenseLinks = {
|
export const LicenseLinks = {
|
||||||
|
|||||||
@@ -2,6 +2,5 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
export type CompleteOnboardingRequest = {
|
export type CompleteOnboardingRequest = {
|
||||||
organization: string;
|
|
||||||
install_plugins: string[];
|
install_plugins: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user