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>
Этот коммит содержится в:
@@ -2349,3 +2349,255 @@ func (a *App) applyPostWillBeConsumedHook(post **model.Post) {
|
||||
return true
|
||||
}, plugin.MessagesWillBeConsumedID)
|
||||
}
|
||||
|
||||
func makePostLink(siteURL, teamName, postID string) string {
|
||||
return fmt.Sprintf("%s/%s/pl/%s", siteURL, teamName, postID)
|
||||
}
|
||||
|
||||
// validateMoveOrCopy performs validation on a provided post list to determine
|
||||
// if all permissions are in place to allow the for the posts to be moved or
|
||||
// copied.
|
||||
func (a *App) ValidateMoveOrCopy(c request.CTX, wpl *model.WranglerPostList, originalChannel *model.Channel, targetChannel *model.Channel, user *model.User) error {
|
||||
if wpl.NumPosts() == 0 {
|
||||
return errors.New("The wrangler post list contains no posts")
|
||||
}
|
||||
|
||||
config := a.Config().WranglerSettings
|
||||
|
||||
switch originalChannel.Type {
|
||||
case model.ChannelTypePrivate:
|
||||
if !*config.MoveThreadFromPrivateChannelEnable {
|
||||
return errors.New("Wrangler is currently configured to not allow moving posts from private channels")
|
||||
}
|
||||
case model.ChannelTypeDirect:
|
||||
if !*config.MoveThreadFromDirectMessageChannelEnable {
|
||||
return errors.New("Wrangler is currently configured to not allow moving posts from direct message channels")
|
||||
}
|
||||
case model.ChannelTypeGroup:
|
||||
if !*config.MoveThreadFromGroupMessageChannelEnable {
|
||||
return errors.New("Wrangler is currently configured to not allow moving posts from group message channels")
|
||||
}
|
||||
}
|
||||
|
||||
if !originalChannel.IsGroupOrDirect() && !targetChannel.IsGroupOrDirect() {
|
||||
// DM and GM channels are "teamless" so it doesn't make sense to check
|
||||
// the MoveThreadToAnotherTeamEnable config when dealing with those.
|
||||
if !*config.MoveThreadToAnotherTeamEnable && targetChannel.TeamId != originalChannel.TeamId {
|
||||
return errors.New("Wrangler is currently configured to not allow moving messages to different teams")
|
||||
}
|
||||
}
|
||||
|
||||
if *config.MoveThreadMaxCount != int64(0) && *config.MoveThreadMaxCount < int64(wpl.NumPosts()) {
|
||||
return fmt.Errorf("the thread is %d posts long, but this command is configured to only move threads of up to %d posts", wpl.NumPosts(), *config.MoveThreadMaxCount)
|
||||
}
|
||||
|
||||
_, appErr := a.GetChannelMember(c, targetChannel.Id, user.Id)
|
||||
if appErr != nil {
|
||||
return fmt.Errorf("channel with ID %s doesn't exist or you are not a member", targetChannel.Id)
|
||||
}
|
||||
|
||||
_, appErr = a.GetChannelMember(c, originalChannel.Id, user.Id)
|
||||
if appErr != nil {
|
||||
return fmt.Errorf("channel with ID %s doesn't exist or you are not a member", originalChannel.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) CopyWranglerPostlist(c request.CTX, wpl *model.WranglerPostList, targetChannel *model.Channel) (*model.Post, *model.AppError) {
|
||||
var appErr *model.AppError
|
||||
var newRootPost *model.Post
|
||||
|
||||
if wpl.ContainsFileAttachments() {
|
||||
// The thread contains at least one attachment. To properly move the
|
||||
// thread, the files will have to be re-uploaded. This is completed
|
||||
// before any messages are moved.
|
||||
// TODO: check number of files that need to be re-uploaded or file size?
|
||||
c.Logger().Info("Wrangler is re-uploading file attachments",
|
||||
mlog.String("file_count", fmt.Sprintf("%d", wpl.FileAttachmentCount)),
|
||||
)
|
||||
|
||||
for _, post := range wpl.Posts {
|
||||
var newFileIDs []string
|
||||
var fileBytes []byte
|
||||
var oldFileInfo, newFileInfo *model.FileInfo
|
||||
for _, fileID := range post.FileIds {
|
||||
oldFileInfo, appErr = a.GetFileInfo(c, fileID)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
fileBytes, appErr = a.GetFile(c, fileID)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
newFileInfo, appErr = a.UploadFile(c, fileBytes, targetChannel.Id, oldFileInfo.Name)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
newFileIDs = append(newFileIDs, newFileInfo.Id)
|
||||
}
|
||||
|
||||
post.FileIds = newFileIDs
|
||||
}
|
||||
}
|
||||
|
||||
for i, post := range wpl.Posts {
|
||||
var reactions []*model.Reaction
|
||||
|
||||
// Store reactions to be reapplied later.
|
||||
reactions, appErr = a.GetReactionsForPost(post.Id)
|
||||
if appErr != nil {
|
||||
// Reaction-based errors are logged, but do not abort
|
||||
c.Logger().Error("Failed to get reactions on original post")
|
||||
}
|
||||
|
||||
newPost := post.Clone()
|
||||
newPost = newPost.CleanPost()
|
||||
newPost.ChannelId = targetChannel.Id
|
||||
|
||||
if i == 0 {
|
||||
newPost, appErr = a.CreatePost(c, newPost, targetChannel, false, false)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
newRootPost = newPost.Clone()
|
||||
} else {
|
||||
newPost.RootId = newRootPost.Id
|
||||
newPost, appErr = a.CreatePost(c, newPost, targetChannel, false, false)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
}
|
||||
|
||||
for _, reaction := range reactions {
|
||||
reaction.PostId = newPost.Id
|
||||
_, appErr = a.SaveReactionForPost(c, reaction)
|
||||
if appErr != nil {
|
||||
// Reaction-based errors are logged, but do not abort
|
||||
c.Logger().Error("Failed to reapply reactions to post")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newRootPost, nil
|
||||
}
|
||||
|
||||
func (a *App) MoveThread(c request.CTX, postID string, sourceChannelID, channelID string, user *model.User) *model.AppError {
|
||||
postListResponse, appErr := a.GetPostThread(postID, model.GetPostsOptions{}, user.Id)
|
||||
if appErr != nil {
|
||||
return model.NewAppError("getPostThread", "app.post.move_thread_command.error", nil, "postID="+postID+", "+"UserId="+user.Id+"", http.StatusBadRequest)
|
||||
}
|
||||
wpl := postListResponse.BuildWranglerPostList()
|
||||
|
||||
originalChannel, appErr := a.GetChannel(c, sourceChannelID)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
targetChannel, appErr := a.GetChannel(c, channelID)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
err := a.ValidateMoveOrCopy(c, wpl, originalChannel, targetChannel, user)
|
||||
if err != nil {
|
||||
return model.NewAppError("validateMoveOrCopy", "app.post.move_thread_command.error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var targetTeam *model.Team
|
||||
if targetChannel.IsGroupOrDirect() {
|
||||
if !originalChannel.IsGroupOrDirect() {
|
||||
targetTeam, appErr = a.GetTeam(originalChannel.TeamId)
|
||||
}
|
||||
} else {
|
||||
targetTeam, appErr = a.GetTeam(targetChannel.TeamId)
|
||||
}
|
||||
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
if targetTeam == nil {
|
||||
return model.NewAppError("validateMoveOrCopy", "app.post.move_thread_command.error", nil, "target team is nil", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Begin creating the new thread.
|
||||
c.Logger().Info("Wrangler is moving a thread", mlog.String("user_id", user.Id), mlog.String("original_post_id", wpl.RootPost().Id), mlog.String("original_channel_id", originalChannel.Id))
|
||||
|
||||
// To simulate the move, we first copy the original messages(s) to the
|
||||
// new channel and later delete the original messages(s).
|
||||
newRootPost, appErr := a.CopyWranglerPostlist(c, wpl, targetChannel)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
T, err := i18n.GetTranslationsBySystemLocale()
|
||||
if err != nil {
|
||||
return model.NewAppError("MoveThread", "app.post.move_thread_command.error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
ephemeralPostProps := model.StringInterface{
|
||||
"TranslationID": "app.post.move_thread.from_another_channel",
|
||||
}
|
||||
_, appErr = a.CreatePost(c, &model.Post{
|
||||
UserId: user.Id,
|
||||
Type: model.PostTypeWrangler,
|
||||
RootId: newRootPost.Id,
|
||||
ChannelId: channelID,
|
||||
Message: T("app.post.move_thread.from_another_channel"),
|
||||
Props: ephemeralPostProps,
|
||||
}, targetChannel, false, false)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
// Cleanup is handled by simply deleting the root post. Any comments/replies
|
||||
// are automatically marked as deleted for us.
|
||||
_, appErr = a.DeletePost(c, wpl.RootPost().Id, user.Id)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
c.Logger().Info("Wrangler thread move complete", mlog.String("user_id", user.Id), mlog.String("new_post_id", newRootPost.Id), mlog.String("channel_id", channelID))
|
||||
|
||||
// Translate to the system locale, webapp will attempt to render in each user's specific locale (based on the TranslationID prop) before falling back on the initiating user's locale
|
||||
ephemeralPostProps = model.StringInterface{}
|
||||
|
||||
msg := T("app.post.move_thread_command.direct_or_group.multiple_messages", model.StringInterface{"NumMessages": wpl.NumPosts()})
|
||||
ephemeralPostProps["TranslationID"] = "app.post.move_thread_command.direct_or_group.multiple_messages"
|
||||
if wpl.NumPosts() == 1 {
|
||||
msg = T("app.post.move_thread_command.direct_or_group.one_message")
|
||||
ephemeralPostProps["TranslationID"] = "app.post.move_thread_command.direct_or_group.one_message"
|
||||
}
|
||||
|
||||
if targetChannel.TeamId != "" {
|
||||
targetTeam, teamErr := a.GetTeam(targetChannel.TeamId)
|
||||
if teamErr != nil {
|
||||
return teamErr
|
||||
}
|
||||
targetName := targetTeam.Name
|
||||
newPostLink := makePostLink(*a.Config().ServiceSettings.SiteURL, targetName, newRootPost.Id)
|
||||
msg = T("app.post.move_thread_command.channel.multiple_messages", model.StringInterface{"NumMessages": wpl.NumPosts(), "Link": newPostLink})
|
||||
ephemeralPostProps["TranslationID"] = "app.post.move_thread_command.channel.multiple_messages"
|
||||
if wpl.NumPosts() == 1 {
|
||||
msg = T("app.post.move_thread_command.channel.one_message", model.StringInterface{"Link": newPostLink})
|
||||
ephemeralPostProps["TranslationID"] = "app.post.move_thread_command.channel.one_message"
|
||||
}
|
||||
ephemeralPostProps["MovedThreadPermalink"] = newPostLink
|
||||
}
|
||||
|
||||
ephemeralPostProps["NumMessages"] = wpl.NumPosts()
|
||||
|
||||
_, appErr = a.CreatePost(c, &model.Post{
|
||||
UserId: user.Id,
|
||||
Type: model.PostTypeWrangler,
|
||||
ChannelId: originalChannel.Id,
|
||||
Message: msg,
|
||||
Props: ephemeralPostProps,
|
||||
}, originalChannel, false, false)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
c.Logger().Info(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user