List & Form Portlets
Build interactive portlets that display data and accept user input.
List Portlet Components
Column Types
| Type | Description | Alignment |
|---|---|---|
text | Plain text | LEFT |
currency | Currency formatted | RIGHT |
date | Date formatted | CENTER |
integer | Whole numbers | RIGHT |
float | Decimal numbers | RIGHT |
percent | Percentage | RIGHT |
url | Clickable link | CENTER |
email | Email link | LEFT |
phone | Phone link | LEFT |
Adding Columns
portlet.addColumn({
id: 'tranid', // Column ID
type: 'text', // Column type
label: 'Order #', // Display label
align: 'LEFT' // LEFT, CENTER, RIGHT
});
Complete List Portlet
/**
* @NApiVersion 2.1
* @NScriptType Portlet
*/
define(['N/search', 'N/url', 'N/format'],
(search, url, format) => {
const render = (params) => {
const portlet = params.portlet;
portlet.title = 'Open Invoices Aging';
// Define columns
portlet.addColumn({ id: 'view', type: 'url', label: ' ', align: 'CENTER' });
portlet.addColumn({ id: 'customer', type: 'text', label: 'Customer', align: 'LEFT' });
portlet.addColumn({ id: 'invoice', type: 'text', label: 'Invoice #', align: 'LEFT' });
portlet.addColumn({ id: 'duedate', type: 'date', label: 'Due Date', align: 'CENTER' });
portlet.addColumn({ id: 'daysoverdue', type: 'integer', label: 'Days', align: 'CENTER' });
portlet.addColumn({ id: 'amount', type: 'currency', label: 'Amount', align: 'RIGHT' });
portlet.addColumn({ id: 'status', type: 'text', label: 'Status', align: 'CENTER' });
// Search for overdue invoices
const invoiceSearch = search.create({
type: search.Type.INVOICE,
filters: [
['mainline', 'is', 'T'],
'AND',
['status', 'anyof', ['CustInvc:A']], // Open invoices
'AND',
['duedate', 'before', 'today']
],
columns: [
search.createColumn({ name: 'entity' }),
search.createColumn({ name: 'tranid' }),
search.createColumn({ name: 'duedate', sort: search.Sort.ASC }),
search.createColumn({ name: 'daysoverdue' }),
search.createColumn({ name: 'amountremaining' })
]
});
let count = 0;
invoiceSearch.run().each((result) => {
if (count >= 15) return false;
const daysOverdue = parseInt(result.getValue('daysoverdue')) || 0;
const recordUrl = url.resolveRecord({
recordType: 'invoice',
recordId: result.id
});
// Determine status color
let statusHtml = '';
if (daysOverdue > 60) {
statusHtml = '<span style="color: #e74c3c; font-weight: bold;">Critical</span>';
} else if (daysOverdue > 30) {
statusHtml = '<span style="color: #f39c12;">Warning</span>';
} else {
statusHtml = '<span style="color: #f1c40f;">Due</span>';
}
portlet.addRow({
view: `<a href="${recordUrl}">View</a>`,
customer: result.getText('entity'),
invoice: result.getValue('tranid'),
duedate: result.getValue('duedate'),
daysoverdue: daysOverdue,
amount: result.getValue('amountremaining'),
status: statusHtml
});
count++;
return true;
});
// Add summary row
if (count === 0) {
portlet.addLine({ text: '<em>No overdue invoices</em>' });
} else {
const allInvoicesUrl = url.resolveTaskLink('LIST_CUSTINVC');
portlet.addLine({
text: `<a href="${allInvoicesUrl}">View All Invoices →</a>`
});
}
};
return { render };
});
Form Portlet Components
Field Types
| Type | Description |
|---|---|
text | Text input |
email | Email input |
phone | Phone input |
integer | Whole number |
float | Decimal number |
currency | Currency input |
date | Date picker |
datetime | Date + time picker |
select | Dropdown list |
multiselect | Multi-select list |
checkbox | Boolean checkbox |
textarea | Multi-line text |
richtext | HTML editor |
Adding Form Fields
// Text field
portlet.addField({
id: 'custpage_name',
type: 'text',
label: 'Name'
});
// Select field with source
portlet.addField({
id: 'custpage_customer',
type: 'select',
label: 'Customer',
source: 'customer'
});
// Date field
portlet.addField({
id: 'custpage_date',
type: 'date',
label: 'Date'
});
Complete Form Portlet
/**
* @NApiVersion 2.1
* @NScriptType Portlet
*/
define(['N/ui/serverWidget', 'N/url', 'N/search'],
(serverWidget, url, search) => {
const render = (params) => {
const portlet = params.portlet;
portlet.title = 'Quick Order Entry';
// Customer select
const customerField = portlet.addField({
id: 'custpage_customer',
type: 'select',
label: 'Customer'
});
customerField.isMandatory = true;
// Add customer options
customerField.addSelectOption({ value: '', text: '- Select Customer -' });
search.create({
type: search.Type.CUSTOMER,
filters: [['isinactive', 'is', 'F']],
columns: ['entityid', 'companyname']
}).run().each((result) => {
customerField.addSelectOption({
value: result.id,
text: result.getValue('companyname') || result.getValue('entityid')
});
return true;
});
// Item select
const itemField = portlet.addField({
id: 'custpage_item',
type: 'select',
label: 'Item',
source: 'item'
});
itemField.isMandatory = true;
// Quantity
const qtyField = portlet.addField({
id: 'custpage_qty',
type: 'integer',
label: 'Quantity'
});
qtyField.isMandatory = true;
qtyField.defaultValue = '1';
// Location
portlet.addField({
id: 'custpage_location',
type: 'select',
label: 'Location',
source: 'location'
});
// Memo
portlet.addField({
id: 'custpage_memo',
type: 'textarea',
label: 'Notes'
});
// Submit URL
const submitUrl = url.resolveScript({
scriptId: 'customscript_quick_order_sl',
deploymentId: 'customdeploy_quick_order_sl'
});
portlet.setSubmitButton({
url: submitUrl,
label: 'Create Order'
});
};
return { render };
});
Form Submission Handler (Suitelet)
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record', 'N/redirect', 'N/ui/message'],
(record, redirect, message) => {
const onRequest = (context) => {
if (context.request.method !== 'POST') {
// Redirect back if not POST
redirect.toTaskLink({ id: 'CARD_-29' }); // Home dashboard
return;
}
const params = context.request.parameters;
try {
// Create Sales Order
const salesOrder = record.create({
type: record.Type.SALES_ORDER,
isDynamic: true
});
salesOrder.setValue('entity', params.custpage_customer);
salesOrder.setValue('memo', params.custpage_memo || '');
if (params.custpage_location) {
salesOrder.setValue('location', params.custpage_location);
}
// Add item line
salesOrder.selectNewLine({ sublistId: 'item' });
salesOrder.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'item',
value: params.custpage_item
});
salesOrder.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity',
value: params.custpage_qty
});
salesOrder.commitLine({ sublistId: 'item' });
const orderId = salesOrder.save();
// Redirect to the new order
redirect.toRecord({
type: record.Type.SALES_ORDER,
id: orderId
});
} catch (e) {
log.error('Quick Order Error', e.message);
// Redirect back with error
redirect.toTaskLink({ id: 'CARD_-29' });
}
};
return { onRequest };
});
Editable List Portlet
/**
* @NApiVersion 2.1
* @NScriptType Portlet
*/
define(['N/search', 'N/url'],
(search, url) => {
const render = (params) => {
const portlet = params.portlet;
portlet.title = 'Task Quick Edit';
// Add editable columns
portlet.addEditColumn({
id: 'complete',
type: 'checkbox',
label: 'Done',
align: 'CENTER'
});
portlet.addColumn({ id: 'title', type: 'text', label: 'Task', align: 'LEFT' });
portlet.addColumn({ id: 'priority', type: 'text', label: 'Priority', align: 'CENTER' });
portlet.addColumn({ id: 'duedate', type: 'date', label: 'Due', align: 'CENTER' });
portlet.addEditColumn({
id: 'status',
type: 'select',
label: 'Status',
align: 'CENTER',
source: 'customlist_task_status'
});
// Get tasks
search.create({
type: search.Type.TASK,
filters: [
['assigned', 'is', '@CURRENT@'],
'AND',
['status', 'anyof', ['NOTSTART', 'PROGRESS']]
],
columns: ['title', 'priority', 'duedate', 'status']
}).run().each((result) => {
portlet.addRow({
complete: 'F',
title: result.getValue('title'),
priority: result.getText('priority'),
duedate: result.getValue('duedate'),
status: result.getValue('status')
});
return true;
});
// Submit handler
portlet.setSubmitButton({
url: url.resolveScript({
scriptId: 'customscript_task_update_sl',
deploymentId: 'customdeploy_task_update_sl'
}),
label: 'Save Changes'
});
};
return { render };
});
Drill-Down Links
// Link to record
const recordUrl = url.resolveRecord({
recordType: 'salesorder',
recordId: recordId,
isEditMode: false
});
// Link to search results
const searchUrl = url.resolveScript({
scriptId: 'customscript_search_results',
deploymentId: 'customdeploy_search_results',
params: { filter: 'open' }
});
// Link to standard list
const listUrl = url.resolveTaskLink({
id: 'LIST_SALESORD'
});
// Add as clickable cell
portlet.addRow({
tranid: `<a href="${recordUrl}">${tranid}</a>`,
// ...
});
Conditional Formatting
// Color-coded status
const getStatusHtml = (status, value) => {
const colors = {
'overdue': '#e74c3c',
'warning': '#f39c12',
'ontrack': '#27ae60'
};
return `<span style="
background: ${colors[status]};
color: white;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
">${value}</span>`;
};
// Usage
portlet.addRow({
status: getStatusHtml('overdue', 'Past Due'),
amount: `<span style="color: #e74c3c; font-weight: bold;">$${amount}</span>`
});
See Also
- Script Portlets - Complete script examples
- Dashboard Best Practices - Design guidelines