Skip to main content

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

TargetNotes
List Bulk ActionWidget appears in the list view bulk action bar
CustomActionCan also be a custom action on a view layout

Pattern Structure

  1. Receive selected instances from pageContext.selectedInstances
  2. Render a target selector (dropdown) and an Execute button
  3. On Execute: open a MuiDialog and render one PerInstanceOperation child per selected instance
  4. Each PerInstanceOperation makes its own useProtrakApi call and reports its status
  5. 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 useProtrakApi calls — each PerInstanceOperation mounts and immediately fires its own API call. This provides natural parallelism.
  • Completion tracking — use a counter (completedCount) incremented by each child's onComplete callback. Only when all instances have completed (success or failure) should the OK button be enabled.
  • onComplete is 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