How to call modal window instead of message with blocks in Slack app? - slack

After command '/day-off' in my Slack app
this code calls message with blocks. But I need a modal window with same blocks. How to do that?
let newTimeOff = 'test';
if (ctx.state.isSlackBot) {
newTimeOff = {
blocks: [
// Start date
{
type: 'input',
block_id: 'start_date_datepicker',
element: {
type: 'datepicker',
initial_date: '1990-04-28',
placeholder: {
type: 'plain_text',
text: 'Select a date',
emoji: true,
},
action_id: 'datepicker-action',
},
label: {
type: 'plain_text',
text: 'Start day',
emoji: true,
},
},
// ... and other inputs
],
};
}
ctx.body = newTimeOff;

'Modal' in Slack have a different workflow than 'Messages'
You can read more about it here:
https://api.slack.com/surfaces/modals/using#creating_modals
In short, you will require https://api.slack.com/methods/views.open method to open 'Modal' windows instead of messages.

Related

Workflow Builder Steps from Apps tutorial: server-side problem with provided glitch code

I’m following the tutorial at Workflow Builder Steps from Apps:
https://api.slack.com/tutorials/workflow-builder-steps
Glitch template:
https://glitch.com/edit/#!/steps-from-apps
I'm not able to verify the Redirect URL in Slack which I think is because I'm seeing a server-side error on glitch.com immediately after remixing the code.
Error message:
ReferenceError: require is not defined
file:///app/index.js:1
const { App, WorkflowStep } = require("#slack/bolt");
ReferenceError: require is not defined
Jump Toat file:///app/index.js:1:31
at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
at async Loader.import (internal/modules/esm/loader.js:166:24)
at async Object.loadESM (internal/process/esm_loader.js:68:5)
Glitch:
https://glitch.com/edit/#!/tarry-tremendous-walnut
Any ideas what’s going on?
Thanks!
Rick
index.js:
const { App, WorkflowStep } = require("#slack/bolt");
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET
});
// This code saves tasks to this object
// Tasks are stored in memory and not in a persistent database
// This object is refreshed each time your Glitch app restarts
// The result is that your App Home will be cleared with each restart
let TASKS_DB = {};
const ws = new WorkflowStep("copy_review", {
edit: async ({ ack, step, configure }) => {
await ack();
const blocks = [
{
type: "section",
block_id: "intro-section",
text: {
type: "plain_text",
text:
"Create a task in one of the listed projects. The link to the task and other details will be available as variable data in later steps.",
emoji: true
}
},
{
type: "input",
block_id: "task_name_input",
element: {
type: "plain_text_input",
action_id: "name",
placeholder: {
type: "plain_text",
text: "Write a task name"
}
},
label: {
type: "plain_text",
text: "Task name",
emoji: true
}
},
{
type: "input",
block_id: "task_description_input",
element: {
type: "plain_text_input",
action_id: "description",
placeholder: {
type: "plain_text",
text: "Write a description for your task"
}
},
label: {
type: "plain_text",
text: "Task description",
emoji: true
}
},
{
type: "input",
block_id: "task_author_input",
element: {
type: "plain_text_input",
action_id: "author",
placeholder: {
type: "plain_text",
text: "Write a task name"
}
},
label: {
type: "plain_text",
text: "Task author",
emoji: true
}
}
];
await configure({ blocks });
},
save: async ({ ack, step, update, view }) => {
await ack();
const {
task_name_input,
task_description_input,
task_author_input
} = view.state.values;
const taskName = task_name_input.name.value;
const taskDescription = task_description_input.description.value;
const taskAuthorEmail = task_author_input.author.value;
const inputs = {
taskName: { value: taskName },
taskDescription: { value: taskDescription },
taskAuthorEmail: { value: taskAuthorEmail }
};
const outputs = [
{
type: "text",
name: "taskName",
label: "Task Name"
},
{
type: "text",
name: "taskDescription",
label: "Task Description"
},
{
type: "text",
name: "taskAuthorEmail",
label: "Task Author Email"
}
];
await update({ inputs, outputs });
},
execute: async ({ step, complete, fail, client }) => {
try {
const { taskName, taskDescription, taskAuthorEmail } = step.inputs;
const outputs = {
taskName: taskName.value,
taskDescription: taskDescription.value,
taskAuthorEmail: taskAuthorEmail.value
};
const user = await client.users.lookupByEmail({
email: taskAuthorEmail.value
});
const userId = user.user.id;
const newTask = {
task_name: taskName.value,
task_description: taskDescription.value
};
TASKS_DB[userId] = TASKS_DB[userId]
? [...TASKS_DB[userId], newTask]
: [newTask];
const taskBlocksItems = TASKS_DB[userId].map(task => {
return [
{
type: "section",
text: {
type: "plain_text",
text: task.task_name,
emoji: true
}
},
{
type: "divider"
}
];
});
const homeHeader = [
{
type: "header",
text: {
type: "plain_text",
text: "Workflow Builder - Steps from Apps task list",
emoji: true
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text:
"_This is a list of tasks generated by your example Workflow Builder Task creation step, found in this tutorial https://api.slack.com/tutorials/workflow-builder-steps. These tasks are stored in the Glitch client, not in a database and refresh every time your Glitch app is restarted. If you'd like to store these tasks in a database, please check out this Glitch page for more information https://glitch.com/#storage _"
}
]
},
{
type: "divider"
}
];
const taskBlocks = [].concat.apply([], taskBlocksItems);
const blocks = homeHeader.concat(taskBlocks);
// update app home
let view = {
type: "home",
blocks
};
const usersTasks = TASKS_DB[userId];
await client.views.publish({
user_id: userId,
view: JSON.stringify(view)
});
// If everything was successful, complete the step
await complete({ outputs });
} catch (e) {
// TODO if something went wrong, fail the step ...
app.logger.error("Error completing step", e.message);
}
}
});
app.step(ws);
(async () => {
// Start your app
const port = process.env.PORT || 3000;
await app.start(port);
console.log(`⚡️ index.js Bolt app is running on port ${port}!`);
})();
Your package.json config has "type": "module" and, according to the original template you're working from, you do not want to be building this script as a module. If you remove that declaration, using require will work.

BotKit + Slack Adapter - How do you delete a conversation when it ends?

I've got the following program, but can't figure out how to delete the buttons, or preferably the whole message, after the user has clicked one of the buttons?
const dialog = new BotkitConversation(BLIND_UPDOWN_DIALOG, controller)
dialog.addQuestion(
{
text: 'What did you want to do?',
action: 'complete',
blocks: async (template, vars) => {
return [
{
type: 'section',
text: {
type: 'plain_text',
text: 'What did you want to do?',
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: '⬆️ Raise blinds',
},
value: 'up',
},
{
type: 'button',
text: {
type: 'plain_text',
text: '⬇️ Lower blinds',
},
value: 'down',
},
{
type: 'button',
text: {
type: 'plain_text',
text: '❌ Cancel',
},
value: 'cancel',
},
],
},
]
},
},
async (response, convo, bot, b) => {
console.log('response', convo, b)
await bot.say('Oh your response is ' + response)
},
'blindCommand'
)
controller.addDialog(dialog)

Botkit - Slack interactive messages

I am trying to get all the values from all actions. Currently I have two select actions (Pick a game and Pick a day). In the interactive_messages_callback I am getting the selected value only of the currently modified select.
Is there a way to get an array of values from all the actions like currentValues: [ os_type_selection: 'osx', day_selection: '2' ]?
bot.reply(message, {
attachments: [
{
title: 'Question 1',
callback_id: 'question_1',
attachment_type: 'default',
actions: [
{
name: 'os_type_selection',
text: 'Pick a game...',
type: 'select',
options: [
{
text: 'Mac OS X',
value: 'osx',
},
{
text: 'Windows',
value: 'windows',
}
]
}
],
},
{
title: 'Question 2',
callback_id: 'question_2',
attachment_type: 'default',
actions: [
{
name: 'day_selection',
text: 'Pick a day...',
type: 'select',
options: [
{
text: 'Monday',
value: '1',
},
{
text: 'Tuesday',
value: '2',
},
]
},
],
},
],
});
// interactive_messages_callback
{ type: 'interactive_message_callback',
actions:
[ { name: 'day_selection',
type: 'select',
selected_options: [Object] } ],
callback_id: 'question_2',
team: { id: 'T02L9R6LX', domain: 'hellephant' },
channel: 'D9066R5NC',
user: 'U4C2DDM9T',
action_ts: '1517489936.972094',
message_ts: '1517489928.000257',
attachment_id: '2',
token: 'f5LpbwCQ2D97BhNOPgn1Gotb',
is_app_unfurl: false,
original_message:
{ type: 'message',
user: 'U90RBPAE6',
text: '...',
bot_id: 'B90UUGKSR',
attachments: [ [Object], [Object] ],
ts: '1517489928.000257' },
response_url: 'https://hooks.slack.com/actions/T02L9R6LX/309104841078/xsmwspjpdhV1oSW06PQkQZp5',
trigger_id: '308368498005.2689856711.9425688de7f023516061a4e4b2701322',
raw_message:
{ type: 'interactive_message',
actions: [ [Object] ],
callback_id: 'question_2',
team: { id: 'T02L9R6LX', domain: 'hellephant' },
channel: { id: 'D9066R5NC', name: 'directmessage' },
user: { id: 'U4C2DDM9T', name: 'davidnovak' },
action_ts: '1517489936.972094',
message_ts: '1517489928.000257',
attachment_id: '2',
token: 'f5LpbwCQ2D97BhNOPgn1Gotb',
is_app_unfurl: false,
original_message:
{ type: 'message',
user: 'U90RBPAE6',
text: '...',
bot_id: 'B90UUGKSR',
attachments: [Object],
ts: '1517489928.000257' },
response_url: 'https://hooks.slack.com/actions/T02L9R6LX/309104841078/xsmwspjpdhV1oSW06PQkQZp5',
trigger_id: '308368498005.2689856711.9425688de7f023516061a4e4b2701322' },
_pipeline: { stage: 'receive' },
text: '2' }
No. You can not have multiple interactive menus on the same message in Slack. Its technically possible, but once the user selects one menu it will always fire for that menu, making it impossible for the user to select from multiple menus at the same time.
If you want to use multiple menus you need to spread them out over separate messages and let the user select one after the other.
Or check out the dialog function, which allows you to use multiple menus at the same time.

Creating a select tag with size>1 CKEDITOR Plugin

My CKEDITOR plugin needs to create <select size="15"><option ...></select>, but the size attribute is not directly supported by the creation mechanism. I have tried various ways of adding the size attribute after creation, but so far no joy. Here is what I have; the select is created but it does not get the size attribute.
CKEDITOR.dialog.add('macrosDialog', function(editor) {
return {
// Basic properties of the dialog window: title, minimum size.
title: 'Cadenza Macros',
resizable: CKEDITOR.DIALOG_RESIZE_BOTH,
minWidth: 400,
minHeight: 200,
// Dialog window contents definition.
contents: [
{
// Definition of the Basic Settings dialog tab (page).
id: 'tab-basic',
label: 'Basic Settings',
// The tab contents.
elements: [
{
type: 'select',
id: 'groups',
name: 'groups',
label: 'Groups',
style: "height: 300",
items: [ [ 'Core Scala' ], [ 'Create Courses with Micronautics Cadenza' ], [ 'Java / Scala Interoperability' ], [ 'Play Framework' ] ],
'default': 'Play Framework'
},
{
// Text input field for the macro title (explanation).
type: 'text',
id: 'macroComment',
label: 'Comment',
validate: CKEDITOR.dialog.validate.notEmpty("Explanation field cannot be empty")
}
]
}
],
onLoad: function(e) {
var groups = editor.document.getElement("groups");
groups.setAttribute("size", 15);
//$("#groups").setAttr("size", 15);
},
onChange: function(e) {
alert('Group: ' + this.getValue());
},
// This method is invoked once a user clicks the OK button, confirming the dialog.
onOk: function() {
// The context of this function is the dialog object itself.
// http://docs.ckeditor.com/#!/api/CKEDITOR.dialog
var dialog = this;
// Creates a new <abbr> element.
var abbr = editor.document.createElement('abbr');
// Set element attribute and text, by getting the defined field values.
abbr.setAttribute('title', dialog.getValueOf('tab-basic', 'title'));
abbr.setText(dialog.getValueOf('tab-basic', 'abbr'));
// Finally, inserts the element at the editor caret position.
editor.insertElement(abbr);
}
};
});
I used the html element to insert whatever I wanted:
contents: [
{
id: 'macrosDialog',
label: 'Basic Settings',
// The tab contents.
elements: [
{
type: 'hbox',
id: 'lists',
//style: "vertical-align: top",
widths: [ '25%', '25%', '25%', '25%' ],
children: [
{
type: 'html',
id: 'groups',
name: 'groups',
html: '<select size="15"></select>'
},{
type: 'html',
id: 'courses',
name: 'courses',
html: '<select size="15"></select>'
},{
type: 'html',
id: 'sections',
name: 'sections',
html: '<select size="15"></select>'
},{
type: 'html',
id: 'lectures',
name: 'lectures',
html: '<select size="15"></select>'
},
],
onLoad: function(data) {
var dialog = this.getDialog();
var groups = dialog.getContentElement('macrosDialog', 'lists', 'groups');
console.log(groups);
var courses = dialog.getContentElement('macrosDialog', 'lists', 'courses');
console.log(courses);
var sections = dialog.getContentElement('macrosDialog', 'lists', 'sections');
console.log(sections);
var lectures = dialog.getContentElement('macrosDialog', 'lists', 'lectures');
console.log(lectures);
}
}
]
}
]

Load another view after login with sencha touch 2.0

I'm having a dumb problem and I would like you to give me a hand.Thanks in advance.
The situatios is as follows: I have 2 wiews (both created with sencha architect 2.0), one for login, and another for general purposes. And I would like to load the second view on successful response when trying to log in, this is, after any successful login. The main problem is that I've tried with Ex.create, Ext.Viewport.add, Ext.Viewport.setActiveItem, but I can't manage to make the second view to appear on screen, the login screen just keeps there and the app does not load the other view. Another thing, I don't have to use a navigation view for this.
Here is the code of my controller, from which I want to load my second view. And as you'll see, I even created a reference of the view, which has autoCreate enabled and has that ID "mainTabPanel":
Ext.define('MyApp.controller.Login', {
extend: 'Ext.app.Controller',
config: {
refs: {
loginButton: {
selector: '#login',
xtype: 'button'
},
username: {
selector: '#user',
xtype: 'textfield'
},
password: {
selector: '#pass',
xtype: 'passwordfield'
},
mainTabPanel: {
selector: '#mainTabPanel',
xtype: 'tabpanel',
autoCreate: true
}
},
control: {
"loginButton": {
tap: 'onLoginButtonTap'
}
}
},
onLoginButtonTap: function(button, e, options) {
Ext.Ajax.request({
url: '../../backend/auth.php',
method: 'POST',
params: {
user: this.getUsername().getValue(),
pass: this.getPassword().getValue()
},
success: function(response) {
var json = Ext.decode(response.responseText);
if (json.type == 'success') {
// LOAD THE DAMN SECOND VIEW HERE!
//var paneltab = Ext.create('MyApp.view.MainTabPanel');
//Ext.Viewport.add(paneltab);
//Ext.Viewport.setActiveItem(this.getMainTabPanel());
} else {
alert(json.value);
}
},
failure: function(response) {
alert('The request failed!');
}
});
}
});
And here is the code of my login view:
Ext.define('MyApp.view.LoginForm', {
extend: 'Ext.form.Panel',
config: {
id: 'loginForm',
ui: 'light',
items: [
{
xtype: 'fieldset',
ui: 'light',
title: 'Log into the system',
items: [
{
xtype: 'textfield',
id: 'user',
label: 'User',
name: 'user'
},
{
xtype: 'passwordfield',
id: 'pass',
label: 'Pass',
name: 'pass'
}
]
},
{
xtype: 'button',
id: 'login',
ui: 'confirm',
text: 'Login'
}
]
}
});
And finally, the code of the view I want to load. This view loads normally if I set it as the Initial View, but does not load when a successful login occurs:
Ext.define('MyApp.view.MainTabPanel', {
extend: 'Ext.tab.Panel',
config: {
id: 'mainTabPanel',
layout: {
animation: 'slide',
type: 'card'
},
items: [
{
xtype: 'container',
layout: {
type: 'vbox'
},
title: 'Tab 1',
iconCls: 'time',
items: [
{
xtype: 'titlebar',
docked: 'top',
title: 'General Report',
items: [
{
xtype: 'button',
iconCls: 'refresh',
iconMask: true,
text: '',
align: 'right'
}
]
},
{
xtype: 'container',
height: 138,
flex: 1,
items: [
{
xtype: 'datepickerfield',
label: 'From',
placeHolder: 'mm/dd/yyyy'
},
{
xtype: 'datepickerfield',
label: 'To',
placeHolder: 'mm/dd/yyyy'
},
{
xtype: 'numberfield',
label: 'Hours'
}
]
},
{
xtype: 'dataview',
ui: 'dark',
itemTpl: [
'<div style="height:50px; background-color: white; margin-bottom: 1px;">',
' <span style="color: #0000FF">{user}</span>',
' <span>{description}</span>',
'</div>'
],
store: 'hoursStore',
flex: 1
}
]
},
{
xtype: 'container',
title: 'Tab 2',
iconCls: 'maps'
},
{
xtype: 'container',
title: 'Tab 3',
iconCls: 'favorites'
}
],
tabBar: {
docked: 'bottom'
}
}
});
Please, I need help... :)
I'm stuck here for like 3 days now and can't figure out what the problem is. Thank you.
Could you post your app.js too?
I'm trying your code here and it load the view as expected. Here is my app.js:
Ext.application({
name: 'MyApp',
requires: [
'Ext.MessageBox'
],
controllers: ['Login'],
views: ['LoginForm','MainTabPanel'],
icon: {
'57': 'resources/icons/Icon.png',
'72': 'resources/icons/Icon~ipad.png',
'114': 'resources/icons/Icon#2x.png',
'144': 'resources/icons/Icon~ipad#2x.png'
},
isIconPrecomposed: true,
startupImage: {
'320x460': 'resources/startup/320x460.jpg',
'640x920': 'resources/startup/640x920.png',
'768x1004': 'resources/startup/768x1004.png',
'748x1024': 'resources/startup/748x1024.png',
'1536x2008': 'resources/startup/1536x2008.png',
'1496x2048': 'resources/startup/1496x2048.png'
},
launch: function() {
// Destroy the #appLoadingIndicator element
Ext.fly('appLoadingIndicator').destroy();
// Initialize the main view
//Ext.Viewport.add(Ext.create('MyApp.view.Main'));
Ext.Viewport.add(Ext.create('MyApp.view.LoginForm'));
},
onUpdated: function() {
Ext.Msg.confirm(
"Application Update",
"This application has just successfully been updated to the latest version. Reload now?",
function(buttonId) {
if (buttonId === 'yes') {
window.location.reload();
}
}
);
}
});
Destroy the login panel, You don't really need it anymore...
success: function(response) {
var json = Ext.decode(response.responseText);
if (json.type == 'success') {
// LOAD THE DAMN SECOND VIEW HERE!
var paneltab = Ext.create('MyApp.view.MainTabPanel');
Ext.getCmp('loginForm').destroy();
Ext.Viewport.add(paneltab);
} else {
alert(json.value);
}
},
There are several problems here:
Your autoCreate rule uses an xtype: 'tabpanel' which will only ever create a vanilla Ext.TabPanel with no items in it. You'd need to assign an xtype attribute to your MyApp.view.MainTabPanel like xtype: 'mainTabPanel' and then use that xtype value in your autoCreate rule.
This then explains why this code won't work since this.getMainTabPanel() will return the wrong object. Simpler would be to just use Ext.Viewport.setActiveItem(paneltab).
In general, you usually don't want assign an id to a view (id: 'mainTabPanel'). Safer to just use an xtype to fetch it. Although you don't need it in this case, avoiding global id's allows you to create multiple instances of a view (or any other class type).

Resources