Bulk Action Widget Pattern
Overview
A widget that operates on multiple selected instances simultaneously. It opens a dialog, lets the user pick a target (e.g., an environment), and then executes the operation for each selected instance — typically by calling a backend program via useProtrakApi. The dialog shows per-instance progress status. This is completely independent of the parent form's save lifecycle.
When to Use
- A list view needs a bulk action button (e.g., "Create Server Details for all selected environments")
- Operations must be performed per-instance with individual success/failure reporting
- The action calls a backend Common Program, not a simple attribute update
Applicable Layout Types
| Target | Notes |
|---|---|
| List Bulk Action | Widget appears in the list view bulk action bar |
CustomAction | Can also be a custom action on a view layout |
Pattern Structure
- Receive selected instances from
pageContext.selectedInstances - Render a target selector (dropdown) and an Execute button
- On Execute: open a
MuiDialogand render onePerInstanceOperationchild per selected instance - Each
PerInstanceOperationmakes its ownuseProtrakApicall and reports its status - Track overall completion count — enable the OK/Close button only when all instances have completed (success or error)
Code Example
function ServerDetailsCreationBulkAction(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Container, Box, Button, ButtonEnums, Dropdown, MuiDialog, Spinner } =
protrakComponents;
const selectedInstances = pageContext.selectedInstances || [];
const [targetPicklist, setTargetPicklist] = React.useState(null);
const [showDialog, setShowDialog] = React.useState(false);
const [isOKDisabled, setIsOKDisabled] = React.useState(true);
const [showReloadIcon, setShowReloadIcon] = React.useState(false);
const environmentOptions = [
{ label: 'Production', value: 'prod-guid' },
{ label: 'Staging', value: 'staging-guid' },
];
const handleExecute = () => {
setIsOKDisabled(true);
setShowReloadIcon(true);
setShowDialog(true);
};
return (
<Box display="flex" direction="column" padding="8px">
<Dropdown
picklistOptions={environmentOptions}
onValueChange={(v) => setTargetPicklist(v?.value || null)}
placeholder="Select Target Environment"
/>
<Button
text="Execute"
appearance={ButtonEnums.Appearance.Primary}
onClick={handleExecute}
disabled={!targetPicklist || selectedInstances.length === 0}
style={{ marginTop: '0.5rem', width: 'fit-content' }}
/>
{showDialog && (
<MuiDialog
dialogTitle={`Processing ${selectedInstances.length} instances...`}
dialogWidth="sm"
leftFooter={
showReloadIcon ? (
<Box>
<i className="fa fa-spinner fa-spin" aria-hidden="true" />
{' Processing...'}
</Box>
) : (
<Box />
)
}
rightFooter={
<Button
text="OK"
appearance={ButtonEnums.Appearance.Primary}
onClick={() => setShowDialog(false)}
disabled={isOKDisabled}
/>
}
>
<BulkStatusGrid
instances={selectedInstances}
selectedPicklist={targetPicklist}
setShowReloadIcon={setShowReloadIcon}
setIsOKButtonDisabled={setIsOKDisabled}
/>
</MuiDialog>
)}
</Box>
);
}
// Renders a table of per-instance operation results
const BulkStatusGrid = ({
instances,
selectedPicklist,
setShowReloadIcon,
setIsOKButtonDisabled,
}) => {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Box } = protrakComponents;
const [completedCount, setCompletedCount] = React.useState(0);
const handleOneComplete = () => {
setCompletedCount((prev) => {
const next = prev + 1;
if (next >= instances.length) {
setShowReloadIcon(false);
setIsOKButtonDisabled(false);
}
return next;
});
};
const headerStyle = {
border: '1px solid #dddddd',
padding: '8px',
backgroundColor: '#e9e6eb',
fontSize: '12px',
fontWeight: 'bold',
};
const cellStyle = {
borderBottom: '1px solid #dddddd',
padding: '8px',
fontSize: '12px',
};
return (
<Box padding="8px">
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={headerStyle}>Instance Name</th>
<th style={headerStyle}>Status</th>
</tr>
</thead>
<tbody>
{instances.map((instance, i) => (
<tr key={i}>
<td style={cellStyle}>{instance.name}</td>
<td style={cellStyle}>
<PerInstanceOperation
instance={instance}
selectedPicklist={selectedPicklist}
onComplete={handleOneComplete}
/>
</td>
</tr>
))}
</tbody>
</table>
</Box>
);
};
// Executes the operation for a single instance and shows its status
const PerInstanceOperation = ({ instance, selectedPicklist, onComplete }) => {
const { protrakUtils, protrakComponents } =
React.useContext(customWidgetContext);
const { useProtrakApi } = protrakUtils;
const { ProtrakFontAwesomeIcon } = protrakComponents;
const [status, setStatus] = React.useState('queued');
const executeOperation = React.useCallback(
({ instance: inst, selectedPicklist: env }) => ({
endpoint:
'programs/CreateAndConnectServerDetailsToEnvironment/executeCommonProgram',
config: {
method: 'POST',
data: [
{
EnvironmentId: env,
RelatedInstance: inst,
},
],
},
}),
[]
);
const { state: opResponse } = useProtrakApi({
requestConfig: executeOperation,
instance,
selectedPicklist,
});
React.useEffect(() => {
if (opResponse.isLoading) {
setStatus('in progress');
} else if (opResponse.isFulfilled && opResponse.data) {
setStatus('success');
onComplete();
} else if (opResponse.isError) {
setStatus(
`failed: ${opResponse.data?.response?.data || 'unknown error'}`
);
onComplete();
}
}, [opResponse]);
const iconStyle = { marginRight: '8px' };
if (status === 'in progress') {
return (
<span>
<ProtrakFontAwesomeIcon
className="fa fa-spinner fa-spin"
aria-hidden="true"
style={{ ...iconStyle, color: '#05C12E' }}
/>
In progress
</span>
);
}
if (status === 'success') {
return (
<span>
<ProtrakFontAwesomeIcon
className="fa fa-check"
aria-hidden="true"
style={{ ...iconStyle, color: '#05C12E' }}
/>
Created successfully
</span>
);
}
return (
<span style={{ color: 'var(--error)', fontWeight: 'bold' }}>
<ProtrakFontAwesomeIcon
className="fa fa-warning"
aria-hidden="true"
style={iconStyle}
/>
{status}
</span>
);
};
Key Points
- Per-instance
useProtrakApicalls — eachPerInstanceOperationmounts and immediately fires its own API call. This provides natural parallelism. - Completion tracking — use a counter (
completedCount) incremented by each child'sonCompletecallback. Only when all instances have completed (success or failure) should the OK button be enabled. onCompleteis always called — even on error, so the dialog does not get stuck.- No
onAttributeEdit— this pattern bypasses the parent form entirely. The operations are direct program executions.
Real Examples
ServerDetailsCreationBulkAction.js— Protrak-Implementation