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