Feature: Wrangler (#23602)
* Migrate feature/wrangler to mono-repo * Add wrangler files * Fix linters, types, etc * Fix snapshots * Fix playwright * Fix pipelines * Fix more pipeline * Fixes for pipelines * More changes for pipeline * Fix types * Add support for a feature flag, but leave it defaulted on for spinwick usage for now * Update snapshot * fix js error when removing last value of multiselect, support CSV marshaling to string array for textsetting * Fix linter * Remove TODO * Remove another TODO * fix tests * Fix i18n * Add server tests * Fix linter * Fix linter * Use proper icon for dot menu * Update snapshot * Add Cypress UI tests for various entrypoints to move thread modal, split SCSS out from forward post into its own thing * clean up * fix linter * More cleanup * Revert files to master * Fix linter for e2e tests * Make ForwardPostChannelSelect channel types configurable with a prop * Add missing return * Fixes from PR feedback * First batch of PR Feedback * Another batch of PR changes * Fix linter * Update snapshots * Wrangler system messages are translated to each user's locale * Initially translate Wrangler into system locale rather than initiating user * More fixes for PR Feedback * Fix some server tests * More updates with master. Fixes around pipelines. Enforce Enterprise license on front/back end * Add tests for dot_menu * More pipeline fixes * Fix e2etests prettier * Update cypress tests, change occurrences of 'Wrangler' with 'Move Thread' * Fix linter * Remove enterprise lock * A couple more occurrences of wrangler strings, and one more enterprise lock * Fix server tests * Fix i18n * Fix e2e linter * Feature flag shouldn't be on by default * Enable move threads feature in smoke tests (#25657) * enable move threads feature * add @prod tag * Fix move_thread_from_public_channel e2e test * Fix e2e style --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
Этот коммит содержится в:
@@ -4184,6 +4184,21 @@ func (c *Client4) GetPostsBefore(ctx context.Context, channelId, postId string,
|
||||
return &list, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// MoveThread moves a thread based on provided post id, and channel id string.
|
||||
func (c *Client4) MoveThread(ctx context.Context, postId string, params *MoveThreadParams) (*Response, error) {
|
||||
js, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, NewAppError("MoveThread", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
r, err := c.DoAPIPost(ctx, c.postRoute(postId)+"/move", string(js))
|
||||
if err != nil {
|
||||
return BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// GetPostsAroundLastUnread gets a list of posts around last unread post by a user in a channel.
|
||||
func (c *Client4) GetPostsAroundLastUnread(ctx context.Context, userId, channelId string, limitBefore, limitAfter int, collapsedThreads bool) (*PostList, *Response, error) {
|
||||
query := fmt.Sprintf("?limit_before=%v&limit_after=%v", limitBefore, limitAfter)
|
||||
|
||||
@@ -3108,6 +3108,51 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) {
|
||||
}
|
||||
}
|
||||
|
||||
type WranglerSettings struct {
|
||||
PermittedWranglerRoles []string
|
||||
AllowedEmailDomain []string
|
||||
MoveThreadMaxCount *int64
|
||||
MoveThreadToAnotherTeamEnable *bool
|
||||
MoveThreadFromPrivateChannelEnable *bool
|
||||
MoveThreadFromDirectMessageChannelEnable *bool
|
||||
MoveThreadFromGroupMessageChannelEnable *bool
|
||||
}
|
||||
|
||||
func (w *WranglerSettings) SetDefaults() {
|
||||
if w.PermittedWranglerRoles == nil {
|
||||
w.PermittedWranglerRoles = make([]string, 0)
|
||||
}
|
||||
if w.AllowedEmailDomain == nil {
|
||||
w.AllowedEmailDomain = make([]string, 0)
|
||||
}
|
||||
if w.MoveThreadMaxCount == nil {
|
||||
w.MoveThreadMaxCount = NewInt64(100)
|
||||
}
|
||||
if w.MoveThreadToAnotherTeamEnable == nil {
|
||||
w.MoveThreadToAnotherTeamEnable = NewBool(false)
|
||||
}
|
||||
if w.MoveThreadFromPrivateChannelEnable == nil {
|
||||
w.MoveThreadFromPrivateChannelEnable = NewBool(false)
|
||||
}
|
||||
if w.MoveThreadFromDirectMessageChannelEnable == nil {
|
||||
w.MoveThreadFromDirectMessageChannelEnable = NewBool(false)
|
||||
}
|
||||
if w.MoveThreadFromGroupMessageChannelEnable == nil {
|
||||
w.MoveThreadFromGroupMessageChannelEnable = NewBool(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WranglerSettings) IsValid() *AppError {
|
||||
validDomainRegex := regexp.MustCompile(`^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$`)
|
||||
for _, domain := range w.AllowedEmailDomain {
|
||||
if !validDomainRegex.MatchString(domain) && domain != "localhost" {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.move_thread.domain_invalid.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GlobalRelayMessageExportSettings struct {
|
||||
CustomerType *string `access:"compliance_compliance_export"` // must be either A9, A10 or CUSTOM, dictates SMTP server url
|
||||
SMTPUsername *string `access:"compliance_compliance_export"`
|
||||
@@ -3405,6 +3450,7 @@ type Config struct {
|
||||
FeatureFlags *FeatureFlags `access:"*_read" json:",omitempty"`
|
||||
ImportSettings ImportSettings // telemetry: none
|
||||
ExportSettings ExportSettings
|
||||
WranglerSettings WranglerSettings
|
||||
}
|
||||
|
||||
func (o *Config) Auditable() map[string]interface{} {
|
||||
@@ -3520,6 +3566,7 @@ func (o *Config) SetDefaults() {
|
||||
}
|
||||
o.ImportSettings.SetDefaults()
|
||||
o.ExportSettings.SetDefaults()
|
||||
o.WranglerSettings.SetDefaults()
|
||||
}
|
||||
|
||||
func (o *Config) IsValid() *AppError {
|
||||
@@ -3610,6 +3657,11 @@ func (o *Config) IsValid() *AppError {
|
||||
if appErr := o.ImportSettings.isValid(); appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
if appErr := o.WranglerSettings.IsValid(); appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,24 @@ func TestConfigOverwriteSignatureAlgorithm(t *testing.T) {
|
||||
require.Equal(t, *c1.SamlSettings.CanonicalAlgorithm, testAlgorithm)
|
||||
}
|
||||
|
||||
func TestWranglerSettingsIsValid(t *testing.T) {
|
||||
// // Test valid domains
|
||||
w := &WranglerSettings{
|
||||
AllowedEmailDomain: []string{"example.com", "subdomain.example.com"},
|
||||
}
|
||||
if err := w.IsValid(); err != nil {
|
||||
t.Errorf("Expected no error for valid domains, but got %v", err)
|
||||
}
|
||||
|
||||
// Test invalid domains
|
||||
w = &WranglerSettings{
|
||||
AllowedEmailDomain: []string{"example", "example..com", "example-.com", "-example.com", "example.com.", "example.com-"},
|
||||
}
|
||||
if err := w.IsValid(); err == nil {
|
||||
t.Errorf("Expected error for invalid domains, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigIsValidDefaultAlgorithms(t *testing.T) {
|
||||
c1 := Config{}
|
||||
c1.SetDefaults()
|
||||
|
||||
@@ -42,6 +42,8 @@ type FeatureFlags struct {
|
||||
|
||||
EnableExportDirectDownload bool
|
||||
|
||||
MoveThreadsEnabled bool
|
||||
|
||||
StreamlinedMarketplace bool
|
||||
|
||||
CloudIPFiltering bool
|
||||
@@ -62,6 +64,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.OnboardingTourTips = true
|
||||
f.CloudReverseTrial = false
|
||||
f.EnableExportDirectDownload = false
|
||||
f.MoveThreadsEnabled = false
|
||||
f.StreamlinedMarketplace = true
|
||||
f.CloudIPFiltering = false
|
||||
f.ConsumePostHook = false
|
||||
|
||||
@@ -45,6 +45,7 @@ const (
|
||||
PostTypeChannelRestored = "system_channel_restored"
|
||||
PostTypeEphemeral = "system_ephemeral"
|
||||
PostTypeChangeChannelPrivacy = "system_change_chan_privacy"
|
||||
PostTypeWrangler = "system_wrangler"
|
||||
PostTypeGMConvertedToChannel = "system_gm_to_channel"
|
||||
PostTypeAddBotTeamsChannels = "add_bot_teams_channels"
|
||||
PostTypeSystemWarnMetricStatus = "warn_metric_status"
|
||||
@@ -193,6 +194,10 @@ type GetPersistentNotificationsPostsParams struct {
|
||||
PerPage int
|
||||
}
|
||||
|
||||
type MoveThreadParams struct {
|
||||
ChannelId string `json:"channel_id"`
|
||||
}
|
||||
|
||||
type SearchParameter struct {
|
||||
Terms *string `json:"terms"`
|
||||
IsOrSearch *bool `json:"is_or_search"`
|
||||
@@ -444,6 +449,7 @@ func (o *Post) IsValid(maxPostSize int) *AppError {
|
||||
PostTypeSystemWarnMetricStatus,
|
||||
PostTypeReminder,
|
||||
PostTypeMe,
|
||||
PostTypeWrangler,
|
||||
PostTypeGMConvertedToChannel:
|
||||
default:
|
||||
if !strings.HasPrefix(o.Type, PostCustomTypePrefix) {
|
||||
@@ -893,3 +899,11 @@ func (o *Post) IsUrgent() bool {
|
||||
|
||||
return *postPriority.Priority == PostPriorityUrgent
|
||||
}
|
||||
|
||||
func (o *Post) CleanPost() *Post {
|
||||
o.Id = ""
|
||||
o.CreateAt = 0
|
||||
o.UpdateAt = 0
|
||||
o.EditAt = 0
|
||||
return o
|
||||
}
|
||||
|
||||
@@ -190,3 +190,39 @@ func (o *PostList) IsChannelId(channelId string) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *PostList) BuildWranglerPostList() *WranglerPostList {
|
||||
wpl := &WranglerPostList{}
|
||||
|
||||
o.UniqueOrder()
|
||||
o.SortByCreateAt()
|
||||
posts := o.ToSlice()
|
||||
|
||||
if len(posts) == 0 {
|
||||
// Something was sorted wrong or an empty PostList was provided.
|
||||
return wpl
|
||||
}
|
||||
|
||||
// A separate ID key map to ensure no duplicates.
|
||||
idKeys := make(map[string]bool)
|
||||
|
||||
for i := range posts {
|
||||
p := posts[len(posts)-i-1]
|
||||
|
||||
// Add UserID to metadata if it's new.
|
||||
if _, ok := idKeys[p.UserId]; !ok {
|
||||
idKeys[p.UserId] = true
|
||||
wpl.ThreadUserIDs = append(wpl.ThreadUserIDs, p.UserId)
|
||||
}
|
||||
|
||||
wpl.FileAttachmentCount += int64(len(p.FileIds))
|
||||
|
||||
wpl.Posts = append(wpl.Posts, p)
|
||||
}
|
||||
|
||||
// Set metadata for earliest and latest posts
|
||||
wpl.EarlistPostTimestamp = wpl.RootPost().CreateAt
|
||||
wpl.LatestPostTimestamp = wpl.Posts[wpl.NumPosts()-1].CreateAt
|
||||
|
||||
return wpl
|
||||
}
|
||||
|
||||
@@ -1016,6 +1016,12 @@ type UsersWithGroupsAndCount struct {
|
||||
Count int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
func (u *User) EmailDomain() string {
|
||||
at := strings.LastIndex(u.Email, "@")
|
||||
// at >= 0 holds true and this is not checked here. It holds true, because during signup we run `mail.ParseAddress(email)`
|
||||
return u.Email[at+1:]
|
||||
}
|
||||
|
||||
type UserPostStats struct {
|
||||
LastLogin int64 `json:"last_login_at,omitempty"`
|
||||
LastStatusAt *int64 `json:"last_status_at,omitempty"`
|
||||
|
||||
33
server/public/model/wrangler.go
Обычный файл
33
server/public/model/wrangler.go
Обычный файл
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
// WranglerPostList provides a list of posts along with metadata about those
|
||||
// posts.
|
||||
type WranglerPostList struct {
|
||||
Posts []*Post
|
||||
ThreadUserIDs []string
|
||||
EarlistPostTimestamp int64
|
||||
LatestPostTimestamp int64
|
||||
FileAttachmentCount int64
|
||||
}
|
||||
|
||||
// NumPosts returns the number of posts in a post list.
|
||||
func (wpl *WranglerPostList) NumPosts() int {
|
||||
return len(wpl.Posts)
|
||||
}
|
||||
|
||||
// RootPost returns the root post in a post list.
|
||||
func (wpl *WranglerPostList) RootPost() *Post {
|
||||
if wpl.NumPosts() < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return wpl.Posts[0]
|
||||
}
|
||||
|
||||
// ContainsFileAttachments returns if the post list contains any file attachments.
|
||||
func (wpl *WranglerPostList) ContainsFileAttachments() bool {
|
||||
return wpl.FileAttachmentCount != 0
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func InitTranslations(serverLocale, clientLocale string) error {
|
||||
defaultClientLocale = clientLocale
|
||||
|
||||
var err error
|
||||
T, err = getTranslationsBySystemLocale()
|
||||
T, err = GetTranslationsBySystemLocale()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func GetTranslationFuncForDir(dir string) (TranslationFuncByLocal, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getTranslationsBySystemLocale() (TranslateFunc, error) {
|
||||
func GetTranslationsBySystemLocale() (TranslateFunc, error) {
|
||||
locale := defaultServerLocale
|
||||
if _, ok := locales[locale]; !ok {
|
||||
mlog.Warn("Failed to load system translations for selected locale, attempting to fall back to default", mlog.String("locale", locale), mlog.String("default_locale", defaultLocale))
|
||||
|
||||
Ссылка в новой задаче
Block a user