Skip to main content

List & Form Portlets

Build interactive portlets that display data and accept user input.


List Portlet Components​

Column Types​

TypeDescriptionAlignment
textPlain textLEFT
currencyCurrency formattedRIGHT
dateDate formattedCENTER
integerWhole numbersRIGHT
floatDecimal numbersRIGHT
percentPercentageRIGHT
urlClickable linkCENTER
emailEmail linkLEFT
phonePhone linkLEFT

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​

TypeDescription
textText input
emailEmail input
phonePhone input
integerWhole number
floatDecimal number
currencyCurrency input
dateDate picker
datetimeDate + time picker
selectDropdown list
multiselectMulti-select list
checkboxBoolean checkbox
textareaMulti-line text
richtextHTML 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 };
});

// 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​