diff --git a/server/command.go b/server/command.go index f53bb1a..622000f 100644 --- a/server/command.go +++ b/server/command.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "regexp" "strings" "time" @@ -49,6 +50,9 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo case "queue": return p.executeCommandQueue(args), nil + case "requeue": + return p.executeCommandReQueue(args), nil + case "setting": return p.executeCommandSetting(args), nil @@ -69,7 +73,7 @@ func (p *Plugin) executeCommandList(args *model.CommandArgs) *model.CommandRespo weekday = int(parsedWeekday) } - hashtag, err := p.GenerateHashtag(args.ChannelId, nextWeek, weekday) + hashtag, err := p.GenerateHashtag(args.ChannelId, nextWeek, weekday, false, time.Now().Weekday()) if err != nil { return responsef("Error calculating hashtags") } @@ -147,25 +151,21 @@ func (p *Plugin) executeCommandQueue(args *model.CommandArgs) *model.CommandResp message = strings.Join(split[3:], " ") } - hashtag, error := p.GenerateHashtag(args.ChannelId, nextWeek, weekday) + hashtag, error := p.GenerateHashtag(args.ChannelId, nextWeek, weekday, false, time.Now().Weekday()) if error != nil { return responsef("Error calculating hashtags. Check the meeting settings for this channel.") } - searchResults, appErr := p.API.SearchPostsInTeamForUser(args.TeamId, args.UserId, model.SearchParameter{Terms: &hashtag}) - - if appErr != nil { - return responsef("Error calculating list number") + itemErr, numQueueItems := calculateQueItemNumber(args, p, hashtag) + if itemErr != nil { + return itemErr } - postList := *searchResults.PostList - numQueueItems := len(postList.Posts) - - _, appErr = p.API.CreatePost(&model.Post{ + _, appErr := p.API.CreatePost(&model.Post{ UserId: args.UserId, ChannelId: args.ChannelId, RootId: args.RootId, - Message: fmt.Sprintf("#### %v %v) %v", hashtag, numQueueItems+1, message), + Message: fmt.Sprintf("#### %v %v) %v", hashtag, numQueueItems, message), }) if appErr != nil { return responsef("Error creating post: %s", appErr.Message) @@ -174,6 +174,86 @@ func (p *Plugin) executeCommandQueue(args *model.CommandArgs) *model.CommandResp return &model.CommandResponse{} } +func calculateQueItemNumber(args *model.CommandArgs, p *Plugin, hashtag string) (*model.CommandResponse, int) { + searchResults, appErr := p.API.SearchPostsInTeamForUser(args.TeamId, args.UserId, model.SearchParameter{Terms: &hashtag}) + if appErr != nil { + return responsef("Error calculating list number"), 0 + } + postList := *searchResults.PostList + numQueueItems := len(postList.Posts) + return nil, numQueueItems + 1 +} + +func (p *Plugin) executeCommandReQueue(args *model.CommandArgs) *model.CommandResponse { + split := strings.Fields(args.Command) + + if len(split) <= 2 { + return responsef("Missing parameters for requeue command") + } + + meeting, err := p.GetMeeting(args.ChannelId) + if err != nil { + return responsef("Can't find the meeting") + } + + oldPostID := split[2] + postToBeReQueued, _ := p.API.GetPost(oldPostID) + var ( + prefix string + hashtagDateFormat string + ) + if matchGroups := meetingDateFormatRegex.FindStringSubmatch(meeting.HashtagFormat); len(matchGroups) == 4 { + prefix = matchGroups[1] + hashtagDateFormat = strings.TrimSpace(matchGroups[2]) + } else { + return responsef("Error 203") + } + + var ( + messageRegexFormat = regexp.MustCompile(fmt.Sprintf(`(?m)^#### #%s(?P.*) [0-9]+\) (?P.*)?$`, prefix)) + ) + + if matchGroups := messageRegexFormat.FindStringSubmatch(postToBeReQueued.Message); len(matchGroups) == 3 { + originalPostDate := strings.ReplaceAll(strings.TrimSpace(matchGroups[1]), "_", " ") // reverse what we do to make it a valid hashtag + originalPostMessage := strings.TrimSpace(matchGroups[2]) + + today := time.Now() + local, _ := time.LoadLocation("Local") + formattedDate, _ := time.ParseInLocation(hashtagDateFormat, originalPostDate, local) + if formattedDate.Year() == 0 { + thisYear := today.Year() + formattedDate = formattedDate.AddDate(thisYear, 0, 0) + } + + if today.Year() <= formattedDate.Year() && today.YearDay() < formattedDate.YearDay() { + return responsef("We don't support re-queuing future items, only available for present and past items.") + } + + hashtag, error := p.GenerateHashtag(args.ChannelId, false, -1, true, formattedDate.Weekday()) + if error != nil { + return responsef("Error calculating hashtags. Check the meeting settings for this channel.") + } + + itemErr, numQueueItems := calculateQueItemNumber(args, p, hashtag) + if itemErr != nil { + return itemErr + } + + _, appErr := p.API.UpdatePost(&model.Post{ + Id: oldPostID, + UserId: args.UserId, + ChannelId: args.ChannelId, + RootId: args.RootId, + Message: fmt.Sprintf("#### %v %v) %v", hashtag, numQueueItems, originalPostMessage), + }) + if appErr != nil { + return responsef("Error creating post: %s", appErr.Message) + } + return &model.CommandResponse{Text: fmt.Sprintf("Item has been Re-queued to %v", hashtag), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + return responsef("Make sure, message is in required format!") +} + func (p *Plugin) executeCommandHelp(args *model.CommandArgs) *model.CommandResponse { return responsef(helpCommandText) } diff --git a/server/meeting.go b/server/meeting.go index eed7c10..93e5c0c 100644 --- a/server/meeting.go +++ b/server/meeting.go @@ -63,7 +63,7 @@ func (p *Plugin) SaveMeeting(meeting *Meeting) error { } // GenerateHashtag returns a meeting hashtag -func (p *Plugin) GenerateHashtag(channelID string, nextWeek bool, weekday int) (string, error) { +func (p *Plugin) GenerateHashtag(channelID string, nextWeek bool, weekday int, requeue bool, assignedDay time.Weekday) (string, error) { meeting, err := p.GetMeeting(channelID) if err != nil { return "", err @@ -76,9 +76,18 @@ func (p *Plugin) GenerateHashtag(channelID string, nextWeek bool, weekday int) ( return "", err } } else { - // Get date for the list of days of the week - if meetingDate, err = nextWeekdayDateInWeek(meeting.Schedule, nextWeek); err != nil { - return "", err + // user didn't provide any specific date, Get date for the list of days of the week + if !requeue { + if meetingDate, err = nextWeekdayDateInWeek(meeting.Schedule, nextWeek); err != nil { + return "", err + } + } else { + if len(meeting.Schedule) == 1 && meeting.Schedule[0] == assignedDay { // if this day is the only day selected in settings + nextWeek = true + } + if meetingDate, err = nextWeekdayDateInWeekSkippingDay(meeting.Schedule, nextWeek, assignedDay); err != nil { + return "", err + } } } diff --git a/server/meeting_test.go b/server/meeting_test.go index a8b5b72..a64d5b6 100644 --- a/server/meeting_test.go +++ b/server/meeting_test.go @@ -138,7 +138,7 @@ func TestPlugin_GenerateHashtag(t *testing.T) { jsonMeeting, err := json.Marshal(tt.args.meeting) tAssert.Nil(err) api.On("KVGet", tt.args.meeting.ChannelID).Return(jsonMeeting, nil) - got, err := mPlugin.GenerateHashtag(tt.args.meeting.ChannelID, tt.args.nextWeek, -1) + got, err := mPlugin.GenerateHashtag(tt.args.meeting.ChannelID, tt.args.nextWeek, -1, false, time.Now().Weekday()) // last parameter doesn't matter unless requeue if (err != nil) != tt.wantErr { t.Errorf("GenerateHashtag() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/server/utils.go b/server/utils.go index 799a076..3a1c3e3 100644 --- a/server/utils.go +++ b/server/utils.go @@ -72,6 +72,25 @@ func nextWeekdayDateInWeek(meetingDays []time.Weekday, nextWeek bool) (*time.Tim return nextWeekdayDate(meetingDay, nextWeek) } +func nextWeekdayDateInWeekSkippingDay(meetingDays []time.Weekday, nextWeek bool, dayToSkip time.Weekday) (*time.Time, error) { + if len(meetingDays) == 0 { + return nil, errors.New("missing weekdays to calculate date") + } + + todayWeekday := time.Now().Weekday() + + // Find which meeting weekday to calculate the date for + meetingDay := meetingDays[0] + for _, day := range meetingDays { + if todayWeekday <= day && day != dayToSkip { + meetingDay = day + break + } + } + + return nextWeekdayDate(meetingDay, nextWeek) +} + // nextWeekdayDate calculates the date of the next given weekday // from today's date. // If nextWeek is true, it will be based on the next calendar week. diff --git a/webapp/src/actions/index.js b/webapp/src/actions/index.js index fa540f7..e4256e8 100644 --- a/webapp/src/actions/index.js +++ b/webapp/src/actions/index.js @@ -1,10 +1,10 @@ import {searchPostsWithParams} from 'mattermost-redux/actions/search'; - +import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {Client4} from 'mattermost-redux/client'; import Client from '../client'; - import ActionTypes from '../action_types'; export function fetchMeetingSettings(channelId = '') { @@ -80,4 +80,33 @@ export function performSearch(terms) { return dispatch(searchPostsWithParams(teamId, {terms, is_or_search: false, include_deleted_channels: viewArchivedChannels, page: 0, per_page: 20}, true)); }; -} \ No newline at end of file +} + +export function requeueItem(itemId) { + return async (dispatch, getState) => { + const command = `/agenda requeue ${itemId}`; + await clientExecuteCommand(dispatch, getState, command); + return {data: true}; + }; +} + +export async function clientExecuteCommand(dispatch, getState, command) { + let currentChannel = getCurrentChannel(getState()); + const currentTeamId = getCurrentTeamId(getState()); + + // Default to town square if there is no current channel (i.e., if Mattermost has not yet loaded) + if (!currentChannel) { + currentChannel = await Client4.getChannelByName(currentTeamId, 'town-square'); + } + + const args = { + channel_id: currentChannel?.id, + team_id: currentTeamId, + }; + + try { + return Client4.executeCommand(command, args); + } catch (error) { + return error; + } +} diff --git a/webapp/src/index.js b/webapp/src/index.js index 3a14357..f22b434 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -1,5 +1,5 @@ -import {updateSearchTerms, updateSearchResultsTerms, updateRhsState, performSearch, openMeetingSettingsModal} from './actions'; +import {updateSearchTerms, updateSearchResultsTerms, updateRhsState, performSearch, openMeetingSettingsModal, requeueItem} from './actions'; import reducer from './reducer'; @@ -19,6 +19,10 @@ export default class Plugin { (channelId) => { store.dispatch(openMeetingSettingsModal(channelId)); }); + + registry.registerPostDropdownMenuAction('Re-queue', (params) => { + store.dispatch(requeueItem(params)); + }); } }