diff --git a/src/routes/interactivity.ts b/src/routes/interactivity.ts index f94028e..e19dff8 100644 --- a/src/routes/interactivity.ts +++ b/src/routes/interactivity.ts @@ -539,7 +539,7 @@ export class InteractivityRoute { switch (callbackId) { case 'newSessionModal:submit': { - return InteractivityRoute.createSession({ payload, team, res }); + return InteractivityRoute.submitNewSessionModal({ payload, team, res }); } default: { @@ -565,7 +565,7 @@ export class InteractivityRoute { /** * A user submits the `new session` modal. */ - static async createSession({ + static async submitNewSessionModal({ payload, // action request payload team, res, @@ -615,13 +615,28 @@ export class InteractivityRoute { }); throw new Error(SessionControllerErrorCode.TITLE_REQUIRED); } - const title = (titleInputState as any)[Object.keys(titleInputState)[0]] + + const rawTitle = (titleInputState as any)[Object.keys(titleInputState)[0]] .value; + if (typeof rawTitle !== 'string') { + throw new Error(SessionControllerErrorCode.TITLE_REQUIRED); + } + + const titles: string[] = []; + rawTitle.split(/\r?\n/).forEach((rawLine) => { + const trimmed = rawLine.trim(); + if (trimmed.length === 0) return; + titles.push(trimmed); + }); - if (!title || title.trim().length == 0) { + if (titles.length === 0) { throw new Error(SessionControllerErrorCode.TITLE_REQUIRED); } + if (titles.length > 10) { + throw new Error(SessionControllerErrorCode.MAX_TITLE_LIMIT_EXCEEDED); + } + ////////////////////////// // Get the participants // ////////////////////////// @@ -729,60 +744,78 @@ export class InteractivityRoute { (option) => option.value == 'average' ); - // Create session struct - const session: ISession = { - id: generateId(), - votingDuration: votingDurationMs, - endsAt: Date.now() + votingDurationMs, - title, - points, - votes: {}, - state: 'active', - teamId: team.id, - channelId, - userId: payload.user.id, - participants, - rawPostMessageResponse: undefined, - protected: isProtected, - average: calculateAverage, - }; + // Async tasks for slack `postMessage` + const tasks = titles.map(async (title) => { + // Create session struct + const session: ISession = { + id: generateId(), + votingDuration: votingDurationMs, + endsAt: Date.now() + votingDurationMs, + title, + points, + votes: {}, + state: 'active', + teamId: team.id, + channelId, + userId: payload.user.id, + participants, + rawPostMessageResponse: undefined, + protected: isProtected, + average: calculateAverage, + }; - logger.info({ - msg: `Creating a new session`, - team: { - id: team.id, - name: team.name, - }, - user: { - id: payload.user.id, - name: payload.user.name, - }, - channelId, - sessionId: session.id, - }); + logger.info({ + msg: `Creating a new session`, + team: { + id: team.id, + name: team.name, + }, + user: { + id: payload.user.id, + name: payload.user.name, + }, + channelId, + sessionId: session.id, + bulkCount: titles.length, + }); - const postMessageResponse = await SessionController.postMessage( - session, - team - ); - session.rawPostMessageResponse = postMessageResponse as any; + const postMessageResponse = await SessionController.postMessage( + session, + team + ); + session.rawPostMessageResponse = postMessageResponse as any; + + SessionStore.upsert(session); + + if (process.env.COUNTLY_APP_KEY) { + Countly.add_event({ + key: 'topic_created', + count: 1, + segmentation: { + participants: session.participants.length, + votingDuration: votingDurationMs, + bulkCount: titles.length, + }, + }); + } + }); - SessionStore.upsert(session); + await Promise.all(tasks); res.send(); const [upsertSettingErr] = await to( - TeamStore.upsertSettings(team.id, session.channelId, { - [ChannelSettingKey.PARTICIPANTS]: session.participants.join(' '), - [ChannelSettingKey.POINTS]: session.points + TeamStore.upsertSettings(team.id, channelId, { + [ChannelSettingKey.PARTICIPANTS]: participants.join(' '), + [ChannelSettingKey.POINTS]: points .map((point) => { if (!point.includes(' ')) return point; if (point.includes(`"`)) return `'${point}'`; return `"${point}"`; }) .join(' '), - [ChannelSettingKey.PROTECTED]: JSON.stringify(session.protected), - [ChannelSettingKey.AVERAGE]: JSON.stringify(session.average), + [ChannelSettingKey.PROTECTED]: JSON.stringify(isProtected), + [ChannelSettingKey.AVERAGE]: JSON.stringify(calculateAverage), [ChannelSettingKey.VOTING_DURATION]: prettyMilliseconds( votingDurationMs ), @@ -791,21 +824,11 @@ export class InteractivityRoute { if (upsertSettingErr) { logger.error({ msg: `Could not upsert settings after creating new session`, - session, + teamId: team.id, + channelId, err: upsertSettingErr, }); } - - if (process.env.COUNTLY_APP_KEY) { - Countly.add_event({ - key: 'topic_created', - count: 1, - segmentation: { - participants: session.participants.length, - votingDuration: votingDurationMs, - }, - }); - } } catch (err) { let shouldLog = true; let logLevel: 'info' | 'warn' | 'error' = 'error'; @@ -874,7 +897,15 @@ export class InteractivityRoute { }; } else if (err.message == SessionControllerErrorCode.TITLE_REQUIRED) { shouldLog = false; - errorMessage = `Title is required`; + errorMessage = `At least one title is required`; + modalErrors = { + title: errorMessage, + }; + } else if ( + err.message == SessionControllerErrorCode.MAX_TITLE_LIMIT_EXCEEDED + ) { + shouldLog = false; + errorMessage = `You can bulk-create up to 10 sessions`; modalErrors = { title: errorMessage, }; diff --git a/src/session/session-controller.ts b/src/session/session-controller.ts index 029b4a9..aa6c548 100644 --- a/src/session/session-controller.ts +++ b/src/session/session-controller.ts @@ -27,6 +27,7 @@ export const DEFAULT_POINTS = [ export enum SessionControllerErrorCode { NO_PARTICIPANTS = 'no_participants', TITLE_REQUIRED = 'title_required', + MAX_TITLE_LIMIT_EXCEEDED = 'max_title_limit_exceeded', UNEXPECTED_PAYLOAD = 'unexpected_payload', INVALID_POINTS = 'invalid_points', SESSION_NOT_ACTIVE = 'session_not_active', @@ -147,6 +148,7 @@ export class SessionController { block_id: 'title', element: { type: 'plain_text_input', + multiline: true, placeholder: { type: 'plain_text', text: 'Write a topic for this voting session', @@ -159,6 +161,12 @@ export class SessionController { text: 'Title', emoji: true, }, + hint: { + type: 'plain_text', + text: + 'You can bulk-create voting sessions, every line will correspond to a new separate session (up to 10)', + emoji: true, + }, }, { type: 'input', @@ -197,14 +205,14 @@ export class SessionController { }) .join(' '), }, - hint: { + label: { type: 'plain_text', - text: 'Enter points separated by space', + text: 'Points', emoji: true, }, - label: { + hint: { type: 'plain_text', - text: 'Points', + text: 'Enter points separated by space', emoji: true, }, }, @@ -573,7 +581,7 @@ function buildMessageAttachmentsForEnding(session: ISession) { name: 'action', text: 'Delete message', type: 'button', - value: JSON.stringify({b: 1}), // button type, 0 => restart, 1 => delete + value: JSON.stringify({ b: 1 }), // button type, 0 => restart, 1 => delete style: 'danger', }, ],