Create and manage Smart Actions

What is a Smart Action?

Sooner or later, you will need to perform actions on your data that are specific to your business. Moderating comments, generating an invoice, logging into a customer’s account or banning a user are exactly the kind of important tasks to unlock in order to manage your day-to-day operations.

On our Live Demo example, our companies collection has many examples of Smart Action. The simplest one is Mark as live.

If you're looking for information on native actions (CRUD), check out the previous page.

Creating a Smart Action

Lumber
Rails
Express/Sequelize
Express/Mongoose

In the following example, the file forest/companies.js contains the Smart Action declaration. After declaring your Smart Action, your Forest UI will be updated with a new button baring the label of this new action.

If you click on this new button, your browser will make an API call POST /forest/actions/mask-as-live. If you want to customize the call, check the list of available options.

The payload of the HTTP request is based on a JSON API document. The data.attributes.ids key allows you to retrieve easily the selected records from the UI and the data.attributes.values key contains all the values of your input fields (handling input values).

payload
{
"data": {
"attributes": {
"ids": ["1985"],
"values": {},
"collection_name": "companies"
},
"type": "custom-action-requests"
}
}

At this time, there’s no Smart Action Implementation because no route in your Admin backend handles the API call yet. In the file /routes/companies.js, we’ve created a new route to implement the business logic behind the “Mark as Live” button.

Forest Admin takes care of the authentication thanks to the Liana.ensureAuthenticated middleware.

You MUST add the CORS headers to enable the domain app.forestadmin.com to trigger API call on your Application URL, which is on a different domain name (e.g. localhost:3000).

The business logic in this Smart Action is extremely simple. We only update here the attribute status of the companies to the value live.

/forest/companies.js
const Liana = require('forest-express-sequelize');
Liana.collection('companies', {
actions: [{
name: 'Mark as Live'
}],
});
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
const models = require('../models');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated,
(req, res) => {
let companyId = req.body.data.attributes.ids[0];
return models.companies
.update({ status: 'live' }, { where: { id: companyId }})
.then(() => {
res.send({ success: 'Company is now live!' });
});
});
module.exports = router;

In the following example, the file lib/forest_liana/collections/company.rb contains the Smart Action declaration. After declaring your Smart Action, your Forest Admin UI will be updated with a new button baring the label of this new action.

If you click on this new button, your browser will make an API call POST /forest/actions/mask-as-live. If you want to customize the call, check the list of available options.

The payload of the HTTP request is based on a JSON API document. The data.attributes.ids key allows you to retrieve easily the selected records from the UI and the data.attributes.values key contains all the values of your input fields (handling input values).

payload
{
"data": {
"attributes": {
"ids": ["1985"],
"values": {},
"collection_name": "companies"
},
"type": "custom-action-requests"
}
}

At this time, there’s no Smart Action Implementation because no route in your Admin backend handles the API call yet. In the file config/routes.rb, we’ve declared a new route and we’ve implemented the business logic behind the “Mark as Live” button in a new controller /app/controllers/forest/companies_controller.rb.

Forest takes care of the authentication thanks to the ForestLiana::ApplicationController parent class controller.

You MUST add the CORS headers to enable the domain app.forestadmin.com to trigger API call on your Application URL, which is on a different domain name (e.g. localhost:3000).

The business logic in this Smart Action is extremely simple. We only update here the attribute status of the companies to the value live.

/lib/forest_liana/collections/company.rb
class Forest::Company
include ForestLiana::Collection
collection :Company
action 'Mark as Live'
end
/config/routes.rb
Rails.application.routes.draw do
# MUST be declared before the mount ForestLiana::Engine.
namespace :forest do
post '/actions/mark-as-live' => 'companies#mark_as_live'
end
mount ForestLiana::Engine => '/forest'
end
/controllers/forest/companies_controller.rb
class Forest::CompaniesController < ForestLiana::ApplicationController
def mark_as_live
company_id = params.dig('data', 'attributes', 'ids').first
Company.update(company_id, status: 'live')
head :no_content
end
end

In the following example, the file forest/companies.js contains the Smart Action declaration. After declaring your Smart Action, your Forest Admin UI will be updated with a new button baring the label of this new action.

If you click on this new button, your browser will make an API call POST /forest/actions/mask-as-live. If you want to customize the call, check the list of available options.

The payload of the HTTP request is based on a JSON API document. The data.attributes.ids key allows you to retrieve easily the selected records from the UI and the data.attributes.values key contains all the values of your input fields (handling input values).

payload
{
"data": {
"attributes": {
"ids": ["1985"],
"values": {},
"collection_name": "companies"
},
"type": "custom-action-requests"
}
}

At this time, there’s no Smart Action Implementation because no route in your Admin backend handles the API call yet. In the file /routes/companies.js, we’ve created a new route to implement the business logic behind the “Mark as Live” button.

Forest Admin takes care of the authentication thanks to the Liana.ensureAuthenticated middleware.

You MUST add the CORS headers to enable the domain app.forestadmin.com to trigger API call on your Application URL, which is on a different domain name (e.g. localhost:3000).

The business logic in this Smart Action is extremely simple. We only update here the attribute status of the companies to the value live.

/forest/companies.js
const Liana = require('forest-express-sequelize');
Liana.collection('companies', {
actions: [{
name: 'Mark as Live'
}],
});
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
const models = require('../models');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated,
(req, res) => {
let companyId = req.body.data.attributes.ids[0];
return models.companies
.update({ status: 'live' }, { where: { id: companyId }})
.then(() => {
res.send({ success: 'Company is now live!' });
});
});
module.exports = router;

In the following example, the file forest/companies.js contains the Smart Action declaration. After declaring your Smart Action, your Forest Admin UI will be updated with a new button baring the label of this new action.

If you click on this new button, your browser will make an API call POST /forest/actions/mask-as-live. If you want to customize the call, check the list of available options.

The payload of the HTTP request is based on a JSON API document. The data.attributes.ids key allows you to retrieve easily the selected records from the UI and the data.attributes.values key contains all the values of your input fields (handling input values).

payload
{
"data": {
"attributes": {
"ids": ["1985"],
"values": {},
"collection_name": "companies"
},
"type": "custom-action-requests"
}
}

At this time, there’s no Smart Action Implementation because no route in your Admin backend handles the API call yet. In the file /routes/companies.js, we’ve created a new route to implement the business logic behind the “Mark as Live” button.

Forest takes care of the authentication thanks to the Liana.ensureAuthenticated middleware.

You MUST add the CORS headers to enable the domain app.forestadmin.com to trigger API call on your Application URL, which is on a different domain name (e.g. localhost:3000).

The business logic in this Smart Action is extremely simple. We only update here the attribute status of the companies to the value live.

/forest/companies.js
const Liana = require('forest-express-mongoose');
Liana.collection('companies', {
actions: [{
name: 'Mark as Live'
}],
});
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-mongoose');
const Company = require('../models/companies');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
let companyId = req.body.data.attributes.ids[0];
return Company
.findOneAndUpdate({ _id: companyId }, { $set: { status: 'live' }})
.then(() => res.send({ success: 'Company is now live!' }));
});
module.exports = router;

Available Smart Action options

Here is the list of available options to customize your Smart Action:

Name

Type

Description

name

string

Label of the action displayed in Forest Admin.

type

string

(optional) Type of the action. Can be bulk, global or single. Default is bulk.

fields

array of objects

(optional) Check the handling input values section.

download

boolean

(optional) If true, the action triggers a file download in the Browser. Default is false

endpoint

string

(optional) Set the API route to call when clicking on the Smart Action. Default is '/forest/actions/name-of-the-action-dasherized'

httpMethod

string

(optional) Set the HTTP method to use when clicking on the Smart Action. Default is POST.

Opening a form to ask input values

Very often, you will need to ask user inputs before triggering the logic behind a Smart Action. For example, you might want to specify a reason if you want to block a user account. Or set the amount to charge a user’s credit card.

Lumber
Rails
Express/Sequelize
Express/Mongoose

On our Live Demo example, we’ve defined 4 input fields on the Smart Action Upload Legal Docs on the collection companies.

/forest/companies.js
const Liana = require('forest-express-sequelize');
Liana.collection('companies', {
actions: [{
name: 'Upload Legal Docs',
type: 'single',
fields: [{
field: 'Certificate of Incorporation',
description: 'The legal document relating to the formation of a company or corporation.',
type: 'File',
isRequired: true
}, {
field: 'Proof of address',
description: '(Electricity, Gas, Water, Internet, Landline & Mobile Phone Invoice / Payment Schedule) no older than 3 months of the legal representative of your company',
type: 'File',
isRequired: true
}, {
field: 'Company bank statement',
description: 'PDF including company name as well as IBAN',
type: 'File',
isRequired: true
}, {
field: 'Valid proof of ID',
description: 'ID card or passport if the document has been issued in the EU, EFTA, or EEA / ID card or passport + resident permit or driving licence if the document has been issued outside the EU, EFTA, or EEA of the legal representative of your company',
type: 'File',
isRequired: true
}],
});
/routes/companies.js
const P = require('bluebird');
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
const models = require('../models');
router.post('/actions/upload-legal-docs', liana.ensureAuthenticated,
(req, res) => {
// Get the current company id
let companyId = req.body.data.attributes.ids[0];
// Get the values of the input fields entered by the admin user.
let attrs = req.body.data.attributes.values;
let certificate_of_incorporation = attrs['Certificate of Incorporation'];
let proof_of_address = attrs['Proof of address'];
let company_bank_statement = attrs['Company bank statement'];
let passport_id = attrs['Valid proof of id'];
// The business logic of the Smart Action. We use the function
// UploadLegalDoc to upload them to our S3 repository. You can see the full
// implementation on our Forest Live Demo repository on Github.
return P.all([
uploadLegalDoc(companyId, certificate_of_incorporation, 'certificate_of_incorporation_id'),
uploadLegalDoc(companyId, proof_of_address, 'proof_of_address_id'),
uploadLegalDoc(companyId, company_bank_statement,'bank_statement_id'),
uploadLegalDoc(companyId, passport_id, 'passport_id'),
])
.then(() => {
// Once the upload is finished, send a success message to the admin user in the UI.
res.send({ success: 'Legal documents are successfully uploaded.' });
});
});
module.exports = router;

On our Live Demo example, we’ve defined 4 input fields on the Smart Action Upload Legal Docs on the collection Company.

/lib/forest_liana/collections/company.rb
class Forest::Company
include ForestLiana::Collection
collection :Company
action 'Upload Legal Docs', type: 'single', fields: [{
field: 'Certificate of Incorporation',
description: 'The legal document relating to the formation of a company or corporation.',
type: 'File',
isRequired: true
}, {
field: 'Proof of address',
description: '(Electricity, Gas, Water, Internet, Landline & Mobile Phone Invoice / Payment Schedule) no older than 3 months of the legal representative of your company',
type: 'File',
isRequired: true
}, {
field: 'Company bank statement',
description: 'PDF including company name as well as IBAN',
type: 'File',
isRequired: true
}, {
field: 'Valid proof of ID',
description: 'ID card or passport if the document has been issued in the EU, EFTA, or EEA / ID card or passport + resident permit or driving licence if the document has been issued outside the EU, EFTA, or EEA of the legal representative of your company',
type: 'File',
isRequired: true
}]
end
/config/routes.rb
Rails.application.routes.draw do
# MUST be declared before the mount ForestLiana::Engine.
namespace :forest do
post '/actions/upload-legal-docs' => 'companies#upload_legal_docs'
end
mount ForestLiana::Engine => '/forest'
end
/app/controllers/forest/companies_controller.rb
class Forest::CompaniesController < ForestLiana::ApplicationController
def upload_legal_doc(company_id, doc, field)
id = SecureRandom.uuid
Forest::S3Helper.new.upload(doc, "livedemo/legal/#{id}")
company = Company.find(company_id)
company[field] = id
company.save
Document.create({
file_id: company[field],
is_verified: true
})
end
def upload_legal_docs
# Get the current company id
company_id = params.dig('data', 'attributes', 'ids')[0]
# Get the values of the input fields entered by the admin user.
attrs = params.dig('data', 'attributes', 'values')
certificate_of_incorporation = attrs['Certificate of Incorporation'];
proof_of_address = attrs['Proof of address'];
company_bank_statement = attrs['Company bank statement'];
passport_id = attrs['Valid proof of ID'];
# The business logic of the Smart Action. We use the function
# upload_legal_doc to upload them to our S3 repository. You can see the
# full implementation on our Forest Live Demo repository on Github.
upload_legal_doc(company_id, certificate_of_incorporation, 'certificate_of_incorporation_id')
upload_legal_doc(company_id, proof_of_address, 'proof_of_address_id')
upload_legal_doc(company_id, company_bank_statement, 'bank_statement_id')
upload_legal_doc(company_id, passport_id, 'passport_id')
# Once the upload is finished, send a success message to the admin user in the UI.
render json: { success: 'Legal documents are successfully uploaded.' }
end
end

On our Live Demo example, we’ve defined 4 input fields on the Smart Action Upload Legal Docs on the collection companies.

/forest/companies.js
const Liana = require('forest-express-sequelize');
Liana.collection('companies', {
actions: [{
name: 'Upload Legal Docs',
type: 'single',
fields: [{
field: 'Certificate of Incorporation',
description: 'The legal document relating to the formation of a company or corporation.',
type: 'File',
isRequired: true
}, {
field: 'Proof of address',
description: '(Electricity, Gas, Water, Internet, Landline & Mobile Phone Invoice / Payment Schedule) no older than 3 months of the legal representative of your company',
type: 'File',
isRequired: true
}, {
field: 'Company bank statement',
description: 'PDF including company name as well as IBAN',
type: 'File',
isRequired: true
}, {
field: 'Valid proof of ID',
description: 'ID card or passport if the document has been issued in the EU, EFTA, or EEA / ID card or passport + resident permit or driving licence if the document has been issued outside the EU, EFTA, or EEA of the legal representative of your company',
type: 'File',
isRequired: true
}],
});
/routes/companies.js
const P = require('bluebird');
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
const models = require('../models');
router.post('/actions/upload-legal-docs', liana.ensureAuthenticated,
(req, res) => {
// Get the current company id
let companyId = req.body.data.attributes.ids[0];
// Get the values of the input fields entered by the admin user.
let attrs = req.body.data.attributes.values;
let certificate_of_incorporation = attrs['Certificate of Incorporation'];
let proof_of_address = attrs['Proof of address'];
let company_bank_statement = attrs['Company bank statement'];
let passport_id = attrs['Valid proof of id'];
// The business logic of the Smart Action. We use the function
// UploadLegalDoc to upload them to our S3 repository. You can see the full
// implementation on our Forest Live Demo repository on Github.
return P.all([
uploadLegalDoc(companyId, certificate_of_incorporation, 'certificate_of_incorporation_id'),
uploadLegalDoc(companyId, proof_of_address, 'proof_of_address_id'),
uploadLegalDoc(companyId, company_bank_statement,'bank_statement_id'),
uploadLegalDoc(companyId, passport_id, 'passport_id'),
])
.then(() => {
// Once the upload is finished, send a success message to the admin user in the UI.
res.send({ success: 'Legal documents are successfully uploaded.' });
});
});
module.exports = router;

On our Live Demo example, we’ve defined 4 input fields on the Smart Action Upload Legal Docs on the collection companies.

/forest/companies.js
const Liana = require('forest-express-mongoose');
Liana.collection('companies', {
actions: [{
name: 'Upload Legal Docs',
type: 'single',
fields: [{
field: 'Certificate of Incorporation',
description: 'The legal document relating to the formation of a company or corporation.',
type: 'File',
isRequired: true
}, {
field: 'Proof of address',
description: '(Electricity, Gas, Water, Internet, Landline & Mobile Phone Invoice / Payment Schedule) no older than 3 months of the legal representative of your company',
type: 'File',
isRequired: true
}, {
field: 'Company bank statement',
description: 'PDF including company name as well as IBAN',
type: 'File',
isRequired: true
}, {
field: 'Valid proof of ID',
description: 'ID card or passport if the document has been issued in the EU, EFTA, or EEA / ID card or passport + resident permit or driving licence if the document has been issued outside the EU, EFTA, or EEA of the legal representative of your company',
type: 'File',
isRequired: true
}],
});
/routes/companies.js
const P = require('bluebird');
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-mongoose');
const models = require('../models');
router.post('/actions/upload-legal-docs', liana.ensureAuthenticated,
(req, res) => {
// Get the current company id
let companyId = req.body.data.attributes.ids[0];
// Get the values of the input fields entered by the admin user.
let attrs = req.body.data.attributes.values;
let certificate_of_incorporation = attrs['Certificate of Incorporation'];
let proof_of_address = attrs['Proof of address'];
let company_bank_statement = attrs['Company bank statement'];
let passport_id = attrs['Valid proof of id'];
// The business logic of the Smart Action. We use the function
// UploadLegalDoc to upload them to our S3 repository. You can see the full
// implementation on our Forest Live Demo repository on Github.
return P.all([
uploadLegalDoc(companyId, certificate_of_incorporation, 'certificate_of_incorporation_id'),
uploadLegalDoc(companyId, proof_of_address, 'proof_of_address_id'),
uploadLegalDoc(companyId, company_bank_statement,'bank_statement_id'),
uploadLegalDoc(companyId, passport_id, 'passport_id'),
])
.then(() => {
// Once the upload is finished, send a success message to the admin user in the UI.
res.send({ success: 'Legal documents are successfully uploaded.' });
});
});
module.exports = router;

Handling input values

Here is the list of available options to customize your input form.

Name

Type

Description

field

string

Label of the input field.

type

string or array

Type of your field.

  • string: Boolean, Date, Dateonly, Enum, File, Number, String

  • array: ['Enum'], ['Number'], ['String']

enums

array of strings

(optional) Required only for the Enum type. This is where you list all the possible values for your input field.

description

array of fields

(optional) Add a description for your admin users to help them fill correctly your form

isRequired

boolean

(optional) If true, your input field will be set as required in the browser. Default is false.

Prefill a form with default values

Forest Admin allows you to set default values to your form. In this example, we will prefill the form with data coming from the record itself (1), with just a few extra lines of code.

Lumber
Rails
Express/Sequelize
Express/Mongoose
/forest/customers.js
const Liana = require('forest-express-sequelize');
const models = require('../models/');
const _ = require('lodash');
Liana.collection('customers', {
actions: [{
name: 'Generate invoice',
download: true
}, {
name: 'Charge credit card',
type: 'single',
fields: [{
field: 'amount',
isRequired: true,
description: 'The amount (USD) to charge the credit card. Example: 42.50',
type: 'Number'
}, {
field: 'description',
isRequired: true,
description: 'Explain the reason why you want to charge manually the customer here',
type: 'String'
}, {
// we added a field to show the full potential of prefilled values in this example
field: 'stripe_id',
isRequired: true,
type: 'String'
}],
// In values you define the appropriate prefilled value of each field
values: (context) => {
return {
amount: 4520,
stripe_id: context.stripe_id,
};
},
}],
// ...
});
/config/routes.rb
Rails.application.routes.draw do
# MUST be declared before the mount ForestLiana::Engine.
namespace :forest do
get '/whoami' => 'admins#whoami'
post '/actions/mark-as-live' => 'companies#mark_as_live'
post '/actions/upload-legal-docs' => 'companies#upload_legal_docs'
post '/actions/charge-credit-card' => 'customers#charge_credit_card'
# the lane below is the only one you need to insert to prefill the smart action
post '/actions/charge-credit-card/values' => 'customers#charge_credit_card_values'
post '/actions/generate-invoice' => 'customers#generate_invoice'
post '/products/actions/import-data' => 'products#import_data'
get '/Product/:product_id/buyers' => 'products#buyers'
# ...
end
/controller/forest/customers_controller.rb
class Forest::CustomersController < ForestLiana::ApplicationController
# ...
def charge_credit_card_values
context = get_smart_action_context
render serializer: nil, json: { amount: 4520, stripe_id: context[:stripe_id] }, status: :ok
end
end
/forest/customers.js
const Liana = require('forest-express-sequelize');
const models = require('../models/');
const _ = require('lodash');
Liana.collection('customers', {
actions: [{
name: 'Generate invoice',
download: true
}, {
name: 'Charge credit card',
type: 'single',
fields: [{
field: 'amount',
isRequired: true,
description: 'The amount (USD) to charge the credit card. Example: 42.50',
type: 'Number'
}, {
field: 'description',
isRequired: true,
description: 'Explain the reason why you want to charge manually the customer here',
type: 'String'
}, {
// we added a field to show the full potential of prefilled values in this example
field: 'stripe_id',
isRequired: true,
type: 'String'
}],
// In values you define the appropriate prefilled value of each field
values: (context) => {
return {
amount: 4520,
stripe_id: context.stripe_id,
};
},
}],
// ...
});
/forest/customers.js
const Liana = require('forest-express-mongoose');
const models = require('../models/');
const _ = require('lodash');
Liana.collection('Customer', {
actions: [{
name: 'Generate invoice',
download: true
}, {
name: 'Charge credit card',
type: 'single',
fields: [{
field: 'amount',
isRequired: true,
description: 'The amount (USD) to charge the credit card. Example: 42.50',
type: 'Number'
}, {
field: 'description',
isRequired: true,
description: 'Explain the reason why you want to charge manually the customer here',
type: 'String'
}, {
// we added a field to show the full potential of prefilled values in this example
field: 'stripe_id',
isRequired: true,
type: 'String'
}],
// In values you define the appropriate prefilled value of each field
values: (context) => {
return {
amount: 4520,
stripe_id: context.stripe_id,
};
},
}],
// ...
});

Values function is available for single type action only.

Customizing response

Default success notification

Returning a 204 status code to the HTTP request of the Smart Action shows the default notification message in the browser.

On our Live Demo example, if our Smart Action Mark as Live route is implemented like this:

Lumber
Rails
Express/Sequelize
Express/Mongoose
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.status(204).send();
});

/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.status(204).send();
});
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-mongoose');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.status(204).send();
});

We will see a success message in the browser:

Custom success notification

If we return a 200 status code with an object { success: '...' } as the payload like this…

Lumber
Rails
Express/Sequelize
Express/Mongoose
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.send({ success: 'Company is now live!' });
});
/app/controllers/forest/companies_controller.rb
class Forest::CompaniesController < ForestLiana::ApplicationController
def mark_as_live
# ...
render json: { success: 'Company is now live!' }
end
end
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.send({ success: 'Company is now live!');
});
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-mongoose');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.send({ success: 'Company is now live!');
});

… the success notification will look like this:

Custom error notification

Finally, returning a 400 status code allows you to return errors properly.

Lumber
Rails
Express/Sequelize
Express/Mongoose
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.status(400).send({ error: 'The company was already live!' });
});
/app/controllers/forest/companies_controller.rb
class Forest::CompaniesController < ForestLiana::ApplicationController
def mark_as_live
# ...
render status: 400, json: { error: 'The company was already live!' }
end
end
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.status(400).send({ error: 'The company was already live!' });
});
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-mongoose');
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
// ...
res.status(400).send({ error: 'The company was already live!' });
});

Custom HTML response

You can also return a HTML page as a response to give more feedback to the admin user who has triggered your Smart Action. To do this, you just need to return a 200 status code with an object { html: '...' }.

On our Live Demo example, we’ve created a Charge credit card Smart Action on the Collection customersthat returns a custom HTML response.

Lumber
Rails
Express/Sequelize
Express/Mongoose
/forest/companies.js
const Liana = require('forest-express-sequelize');
Liana.collection('customers', {
actions: [{
name: 'Charge credit card',
type: 'single',
fields: [{
field: 'amount',
isRequired: true,
description: 'The amount (USD) to charge the credit card. Example: 42.50',
type: 'Number'
}, {
field: 'description',
isRequired: true,
description: 'Explain the reason why you want to charge manually the customer here',
type: 'String'
}]
}]
});
/routes/customers.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
const models = require('../models');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
router.post('/actions/charge-credit-card', Liana.ensureAuthenticated, (req, res) => {
let customerId = req.body.data.attributes.ids[0];
let amount = req.body.data.attributes.values.amount * 100;
let description = req.body.data.attributes.values.description;
return models.customers
.findById(customerId)
.then((customer) => {
return stripe.charges.create({
amount: amount,
currency: 'usd',
customer: customer.stripe_id,
description: description
});
})
.then((response) => {
res.send({
html: `
<p class="c-clr-1-4 l-mt l-mb">\$${response.amount / 100} USD has been successfuly charged.</p>
<strong class="c-form__label--read c-clr-1-2">Credit card</strong>
<p class="c-clr-1-4 l-mb">**** **** **** ${response.source.last4}</p>
<strong class="c-form__label--read c-clr-1-2">Expire</strong>
<p class="c-clr-1-4 l-mb">${response.source.exp_month}/${response.source.exp_year}</p>
<strong class="c-form__label--read c-clr-1-2">Card type</strong>
<p class="c-clr-1-4 l-mb">${response.source.brand}</p>
<strong class="c-form__label--read c-clr-1-2">Country</strong>
<p class="c-clr-1-4 l-mb">${response.source.country}</p>
`
});
});
});
module.exports = router;
/lib/forest_liana/collections/customer.rb
class Forest::Customer
include ForestLiana::Collection
collection :Customer
action 'Charge credit card', type: 'single', fields: [{
field: 'amount',
isRequired: true,
description: 'The amount (USD) to charge the credit card. Example: 42.50',
type: 'Number'
}, {
field: 'description',
isRequired: true,
description: 'Explain the reason why you want to charge manually the customer here',
type: 'String'
}]
end
/config/routes.rb
Rails.application.routes.draw do
# MUST be declared before the mount ForestLiana::Engine.
namespace :forest do
post '/actions/charge-credit-card' => 'customers#charge_credit_card'
end
mount ForestLiana::Engine => '/forest'
end
/app/controllers/forest/customers_controller.rb
class Forest::CustomersController < ForestLiana::ApplicationController
def charge_credit_card
customer_id = params.dig('data', 'attributes', 'ids')[0]
amount = params.dig('data', 'attributes', 'values', 'amount').to_i
description = params.dig('data', 'attributes', 'values', 'description')
customer = Customer.find(customer_id)
response = Stripe::Charge.create(
amount: amount * 100,
currency: 'usd',
customer: customer.stripe_id,
description: description
)
render json: { html: <<EOF
<p class="c-clr-1-4 l-mt l-mb">$#{response.amount / 100.0} USD has been successfuly charged.</p>
<strong class="c-form__label--read c-clr-1-2">Credit card</strong>
<p class="c-clr-1-4 l-mb">**** **** **** #{response.source.last4}</p>
<strong class="c-form__label--read c-clr-1-2">Expire</strong>
<p class="c-clr-1-4 l-mb">#{response.source.exp_month}/#{response.source.exp_year}</p>
<strong class="c-form__label--read c-clr-1-2">Card type</strong>
<p class="c-clr-1-4 l-mb">#{response.source.brand}</p>
<strong class="c-form__label--read c-clr-1-2">Country</strong>
<p class="c-clr-1-4 l-mb">#{response.source.country}</p>
EOF
}
end
end
/forest/companies.js
const Liana = require('forest-express-sequelize');
Liana.collection('customers', {
actions: [{
name: 'Charge credit card',
type: 'single',
fields: [{
field: 'amount',
isRequired: true,
description: 'The amount (USD) to charge the credit card. Example: 42.50',
type: 'Number'
}, {
field: 'description',
isRequired: true,
description: 'Explain the reason why you want to charge manually the customer here',
type: 'String'
}]
}]
});
/routes/customers.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
const models = require('../models');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
router.post('/actions/charge-credit-card', Liana.ensureAuthenticated, (req, res) => {
let customerId = req.body.data.attributes.ids[0];
let amount = req.body.data.attributes.values.amount * 100;
let description = req.body.data.attributes.values.description;
return models.customers
.findById(customerId)
.then((customer) => {
return stripe.charges.create({
amount: amount,
currency: 'usd',
customer: customer.stripe_id,
description: description
});
})
.then((response) => {
res.send({
html: `
<p class="c-clr-1-4 l-mt l-mb">\$${response.amount / 100} USD has been successfuly charged.</p>
<strong class="c-form__label--read c-clr-1-2">Credit card</strong>
<p class="c-clr-1-4 l-mb">**** **** **** ${response.source.last4}</p>
<strong class="c-form__label--read c-clr-1-2">Expire</strong>
<p class="c-clr-1-4 l-mb">${response.source.exp_month}/${response.source.exp_year}</p>
<strong class="c-form__label--read c-clr-1-2">Card type</strong>
<p class="c-clr-1-4 l-mb">${response.source.brand}</p>
<strong class="c-form__label--read c-clr-1-2">Country</strong>
<p class="c-clr-1-4 l-mb">${response.source.country}</p>
`
});
});
});
module.exports = router;
/forest/companies.js
const Liana = require('forest-express-mongoose');
Liana.collection('Customer', {
actions: [{
name: 'Charge credit card',
type: 'single',
fields: [{
field: 'amount',
isRequired: true,
description: 'The amount (USD) to charge the credit card. Example: 42.50',
type: 'Number'
}, {
field: 'description',
isRequired: true,
description: 'Explain the reason why you want to charge manually the customer here',
type: 'String'
}]
}]
});
/routes/customers.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-mongoose');
const Customer = require('../models/customers');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
router.post('/actions/charge-credit-card', Liana.ensureAuthenticated, (req, res) => {
let customerId = req.body.data.attributes.ids[0];
let amount = req.body.data.attributes.values.amount * 100;
let description = req.body.data.attributes.values.description;
return Customer
.findById(customerId)
.then((customer) => {
return stripe.charges.create({
amount: amount,
currency: 'usd',
customer: customer.stripe_id,
description: description
});
})
.then((response) => {
res.send({
html: `
<p class="c-clr-1-4 l-mt l-mb">\$${response.amount / 100} USD has been successfuly charged.</p>
<strong class="c-form__label--read c-clr-1-2">Credit card</strong>
<p class="c-clr-1-4 l-mb">**** **** **** ${response.source.last4}</p>
<strong class="c-form__label--read c-clr-1-2">Expire</strong>
<p class="c-clr-1-4 l-mb">${response.source.exp_month}/${response.source.exp_year}</p>
<strong class="c-form__label--read c-clr-1-2">Card type</strong>
<p class="c-clr-1-4 l-mb">${response.source.brand}</p>
<strong class="c-form__label--read c-clr-1-2">Country</strong>
<p class="c-clr-1-4 l-mb">${response.source.country}</p>
`
});
});
});
module.exports = router;

Downloading a file

Lumber
Rails
Express/Sequelize
Express/Mongoose

On our Live Demo, the collection customers has a Smart Action Generate invoice. In this use case, we want to download the generated PDF invoice after clicking on the action. To indicate a Smart Action returns something to download, you have to enable the option download.

/forest/customers.js
const Liana = require('forest-express-sequelize');
Liana.collection('customers', {
actions: [{
name: 'Generate invoice',
download: true // If true, the action triggers a file download in the Browser.
}]
});
/routes/customers.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
router.post('/actions/generate-invoice', Liana.ensureAuthenticated,
(req, res) => {
let options = {
root: __dirname + '/../public/',
dotfiles: 'deny',
headers: {
'Access-Control-Expose-Headers': 'Content-Disposition',
'Content-Disposition': 'attachment; filename="invoice-2342.pdf"'
}
};
let fileName = 'invoice-2342.pdf';
res.sendFile(fileName, options, (error) => {
if (error) { next(error); }
});
});
module.exports = router;

On our Live Demo, the collection Customer has a Smart Action Generate invoice. In this use case, we want to download the generated PDF invoice after clicking on the action. To indicate a Smart Action returns something to download, you have to enable the option download.

Don’t forget to expose the Content-Disposition header in the CORS configuration (as shown in the code below) to be able to customize the filename to download.

/lib/forest_liana/collections/customer.rb
class Forest::Customer
include ForestLiana::Collection
collection :Customer
action 'Generate invoice', download: true
end
/config/routes.rb
Rails.application.routes.draw do
# MUST be declared before the mount ForestLiana::Engine.
namespace :forest do
post '/actions/generate-invoice' => 'customers#generate_invoice'
end
mount ForestLiana::Engine => '/forest'
end
/config/application.rb
module LiveDemoRails
class Application < Rails::Application
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', :headers => :any, :methods =>