Visual map — Ch.04 · Service Portal — self-service
Widgets hrlx_my_leaves + hrlx_balance → page hrlx_home
Portal suffix hrlx
Development
Full visual path (all chapters): VISUAL_FROM_SCRATCH_00_16.html
Studio-only from scratch: Portal widgets use u_* fields on req_v2 and bal_v2. No external build tools.
Also open: Properties + Direct Links · Schema · Studio Create All Objects · Previous: 03 Service Catalog
How portal objects relate
Portal (url_suffix hrlx)
└── Page hrlx_home
├── Part 1 Widget hrlx_my_leaves (req_v2 list + cancel)
└── Part 2 Widget hrlx_balance (bal_v2 cards)
Property x_786686_hr_leave.config.portal_page_id = hrlx_home
Rule 0 — Before any object (Application + Update Set + Studio)
Repeat these checks before creating or editing any application file. This section is also section 00 Update Set — create the capture set first.
- Application picker — top-right globe / application menu → select HR Leave (
x_786686_hr_leave). Never leave Global selected when creating scoped artifacts. - Update Set — All → System Update Sets → Local Update Sets → New:
After submit: open the record → click Make Current. Direct list: {BASE}/sys_update_set_list.doField Value Name HR Leave – BuildApplication HR Leave State In Progress Description Captures all HR Leave lab artifacts - Open Studio — {BASE}/$studio.do → confirm header shows HR Leave.
If artifacts land in Global scope, delete them and recreate under HR Leave. ACLs and events created globally will not behave correctly for this app.
Verify Update Set: Create a test Script Include in Studio → open HR Leave – Build → Related List Customer Updates shows the new file.
Part 1: Widget — hrlx_my_leaves
Purpose
Lists current user's leave requests with cancel action for draft/submitted rows.
Navigation path
Direct URL
Prerequisites
- Part 3: Table — Leave Request (01).
- Part 1: Roles employee (02).
Form fields
| Field | Value |
|---|---|
| ID | hrlx_my_leaves |
| Name | HRLX My Leaves |
| Client controller | HrlxMyLeavesCtrl |
| Application | HR Leave |
Step-by-step create
- SP → Widgets → New → ID hrlx_my_leaves.
- Paste HTML template, CSS, Client script, Server script from sections below.
- Save.
Full script — hrlx_my_leaves HTML template
<div class="hrlx-my-leaves">
<h3>My Leave Requests</h3>
<table class="table table-striped" ng-if="data.leaves.length">
<thead>
<tr>
<th>Number</th>
<th>Type</th>
<th>Start</th>
<th>End</th>
<th>Days</th>
<th>State</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in data.leaves">
<td>{{item.number}}</td>
<td>{{item.type}}</td>
<td>{{item.start}}</td>
<td>{{item.end}}</td>
<td>{{item.days}}</td>
<td><span class="label label-default">{{item.state}}</span></td>
<td>
<button class="btn btn-xs btn-danger"
ng-if="item.state === 'submitted' || item.state === 'draft'"
ng-click="c.cancelLeave(item)">Cancel</button>
</td>
</tr>
</tbody>
</table>
<p ng-if="!data.leaves.length" class="text-muted">No leave requests yet.</p>
</div>
Full script — hrlx_my_leaves CSS
.hrlx-my-leaves {
padding: 16px;
h3 {
margin-top: 0;
margin-bottom: 16px;
font-weight: 600;
}
.label {
text-transform: capitalize;
}
}
Full script — hrlx_my_leaves Client script
/**
* Widget: hrlx_my_leaves — Client Controller
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
*/
function HrlxMyLeavesCtrl() {
var c = this;
c.cancelLeave = function (item) {
if (!confirm('Cancel leave ' + item.number + '?')) return;
c.data.action = 'cancel';
c.data.sys_id = item.sys_id;
c.server.update().then(function () {
c.data.action = '';
c.data.sys_id = '';
});
};
}
Full script — hrlx_my_leaves Server script
/**
* Widget: hrlx_my_leaves — Server Script
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Shows the current user's leave requests (secure).
*/
(function () {
data.leaves = [];
var gr = new GlideRecordSecure('x_786686_hr_leave_req_v2');
gr.addQuery('u_requested_for_user', gs.getUserID());
gr.orderByDesc('sys_created_on');
gr.setLimit(50);
gr.query();
while (gr.next()) {
data.leaves.push({
sys_id: gr.getUniqueValue(),
number: gr.getValue('u_number'),
type: gr.getDisplayValue('u_leave_type'),
start: gr.getValue('u_start_date'),
end: gr.getValue('u_end_date'),
days: gr.getValue('u_days_count'),
state: gr.getValue('u_state')
});
}
// Cancel action from client
if (input && input.action === 'cancel' && input.sys_id) {
var c = new GlideRecord('x_786686_hr_leave_req_v2');
if (c.get(input.sys_id) && c.getValue('u_requested_for_user') === gs.getUserID()) {
c.u_state = 'cancelled';
c.update();
}
}
})();
Verify checklist
- Widget saves. Preview shows table structure.
Demo / test
After Part 3 page built: employee1 sees own requests.
Part 2: Widget — hrlx_balance
Purpose
Read-only balance cards for current year from bal_v2.
Navigation path
Direct URL
Prerequisites
- Part 4: Table — Leave Balance (01).
- Part 1: Widget hrlx_my_leaves (same portal context).
Form fields
| Field | Value |
|---|---|
| ID | hrlx_balance |
| Name | HRLX Balance |
| Client controller | HrlxBalanceCtrl |
| Application | HR Leave |
Step-by-step create
- SP → Widgets → New → ID hrlx_balance.
- Paste HTML, CSS, Client, Server from below.
- Save.
Full script — hrlx_balance HTML template
<div class="hrlx-balance">
<h3>Leave Balance {{data.year}}</h3>
<div class="hrlx-cards" ng-if="data.balances.length">
<div class="hrlx-card" ng-repeat="b in data.balances">
<div class="hrlx-card-type">{{b.type}}</div>
<div class="hrlx-card-remaining">{{b.remaining}}</div>
<div class="hrlx-card-meta">used {{b.used}} / {{b.entitled}}</div>
</div>
</div>
<p ng-if="!data.balances.length" class="text-muted">No balance records found.</p>
</div>
Full script — hrlx_balance CSS
.hrlx-balance {
padding: 16px;
h3 { margin-top: 0; font-weight: 600; }
.hrlx-cards {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.hrlx-card {
flex: 1 1 120px;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px;
text-align: center;
}
.hrlx-card-type { text-transform: capitalize; color: #707070; }
.hrlx-card-remaining { font-size: 28px; font-weight: 700; }
.hrlx-card-meta { font-size: 12px; color: #909090; }
}
Full script — hrlx_balance Client script
/**
* Widget: hrlx_balance — Client Controller
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
*/
function HrlxBalanceCtrl() {
var c = this;
// No client actions needed; data is read-only.
}
Full script — hrlx_balance Server script
/**
* Widget: hrlx_balance — Server Script
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Shows the current user's remaining leave balance for the current year.
*/
(function () {
data.balances = [];
data.year = new GlideDateTime().getYearLocalTime();
// find employee record of current user
var emp = new GlideRecord('x_786686_hr_leave_employee_v2');
emp.addQuery('u_user', gs.getUserID());
emp.query();
if (!emp.next()) return;
var bal = new GlideRecordSecure('x_786686_hr_leave_bal_v2');
bal.addQuery('u_employee_ref', emp.getUniqueValue());
bal.addQuery('u_year', data.year);
bal.query();
while (bal.next()) {
data.balances.push({
type: bal.getDisplayValue('u_leave_type'),
entitled: parseInt(bal.getValue('u_entitled') || 0, 10),
used: parseInt(bal.getValue('u_used') || 0, 10),
remaining: parseInt(bal.getValue('u_remaining') || 0, 10)
});
}
})();
Verify checklist
- Cards show entitled/used/remaining when bal_v2 seeded.
Demo / test
employee1 with balance row → remaining displays correctly.
Part 3: Portal Page — hrlx_home
Purpose
HR Leave home page with balance widget on top and my leaves below.
Navigation path
Direct URL
Prerequisites
Form fields — Portal (sp_portal)
| Field | Value |
|---|---|
| Title | HR Leave Portal |
| URL suffix | hrlx |
| Application | HR Leave |
Page (sp_page)
| Field | Value |
|---|---|
| ID | hrlx_home |
| Title | HR Leave Home |
| Application | HR Leave |
| Roles | x_786686_hr_leave.employee |
Step-by-step create
- Portals → New OR configure dedicated portal url_suffix
hrlx. - Pages → New → ID hrlx_home → drag hrlx_balance (top), hrlx_my_leaves (below) on 12-column layout.
- Page security → role employee.
- Set property
x_786686_hr_leave.config.portal_page_id= hrlx_home (02 Administration Part 5). - Save. Open {BASE}/hrlx?id=hrlx_home as employee1.
Verify checklist
- employee1 sees own requests and balance.
- Cancel button works on draft/submitted.
Demo / test
Login employee1 → hrlx?id=hrlx_home → requests + balance visible.
Service Portal verify checklist
Complete after Parts 1–3.
- Two widgets scoped to HR Leave.
- Page hrlx_home published with role restriction.
- Portal URL opens for employee1.