Version 2025-07 is the last API version to support React-based UI components. Later versions use web components, native UI elements with built-in accessibility, better performance, and consistent styling with Shopify's design system. Check out the upgrade guide to upgrade your extension.
Orders
Order pages display customer purchases, including line items, payment status, fulfillment details, and shipping information. Extensions help merchants streamline fulfillment workflows, integrate with external logistics systems, and provide real-time order insights. Learn more about managing orders in the Shopify admin.
Anchor to Use casesUse cases
- Fulfillment and shipping integration: Connect orders with external fulfillment providers, 3PLs, or shipping carriers to automate label generation, track shipments, and sync fulfillment status across systems.
- Customer communication workflows: Send order confirmations, shipping updates, or custom notifications through external communication platforms like email services, SMS gateways, or messaging apps.
- Fraud detection and risk assessment: Flag potentially fraudulent orders, display risk scores from external fraud prevention services, or trigger manual review workflows for suspicious transactions.
- Order analytics and reporting: Display order insights, export data to business intelligence tools, or generate custom reports for sales analysis, inventory planning, or financial reconciliation.
- Bulk order operations: Process multiple orders at once for batch fulfillment, mass invoice generation, bulk status updates, or exporting order data to accounting and ERP systems.

Anchor to Order details targetsOrder details targets
Use action and block targets to extend the order details page with workflows and contextual information.
Action targets open as modal overlays from the More actions menu, while block targets display as inline cards. The examples demonstrate fetching data from the direct API or your app's backend.
Anchor to Order details action ,[object Object]Order details action target
admin.order-details.action.render
Renders an admin action extension on the order details page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on order data, such as syncing with external systems, exporting order information, or managing credit terms.
Extensions at this target can access order data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that pushes order data to an external ERP system. This example demonstrates calling your app backend with order details, handling sync options, and providing user feedback on the export status.
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-details.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [syncInventory, setSyncInventory] = useState(true); const [syncCustomer, setSyncCustomer] = useState(true); const [exportFormat, setExportFormat] = useState(['standard']); const handleExport = async () => { setLoading(true); setError(null); const orderId = data.selected[0].id; try { const response = await fetch('https://your-app.com/api/erp/export-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, options: { syncInventory, syncCustomer, format: exportFormat[0], }, }), }); if (response.ok) { setSuccess(true); close(); } else { const result = await response.json(); setError(result.message || 'Failed to export order to ERP'); } } catch (err) { setError('Connection error. Please check your network and try again.'); } finally { setLoading(false); } }; return ( <AdminAction title="Export to ERP System" primaryAction={ <Button onPress={handleExport} disabled={loading || success}> {loading ? 'Exporting...' : 'Export Order'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success">Order successfully exported to ERP system!</Banner> )} {error && <Banner tone="critical">{error}</Banner>} <Box padding="base"> <BlockStack gap="base"> <ChoiceList name="exportFormat" title="Export Format" value={exportFormat} onChange={setExportFormat} choices={[ { label: 'Standard (recommended)', value: 'standard' }, { label: 'Detailed with line items', value: 'detailed' }, { label: 'Minimal (ID only)', value: 'minimal' }, ]} /> <Divider /> <Checkbox checked={syncInventory} onChange={setSyncInventory} > Sync inventory levels after export </Checkbox> <Checkbox checked={syncCustomer} onChange={setSyncCustomer} > Update customer record in ERP </Checkbox> </BlockStack> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let syncInventory = true; let syncCustomer = true; let exportFormat = ['standard']; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Order successfully exported to ERP system!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'exportFormat', title: 'Export Format', value: exportFormat, onChange: (val: string[]) => { exportFormat = val; updateUI(); }, choices: [ { label: 'Standard (recommended)', value: 'standard' }, { label: 'Detailed with line items', value: 'detailed' }, { label: 'Minimal (ID only)', value: 'minimal' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent(Checkbox, { checked: syncInventory, onChange: (val: boolean) => { syncInventory = val; updateUI(); }, }, 'Sync inventory levels after export') ); innerStack.appendChild( root.createComponent(Checkbox, { checked: syncCustomer, onChange: (val: boolean) => { syncCustomer = val; updateUI(); }, }, 'Update customer record in ERP') ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handleExport, disabled: loading || success }, loading ? 'Exporting...' : 'Export Order' ) ); }; const handleExport = async () => { loading = true; error = null; updateUI(); const orderId = api.data.selected[0].id; try { const response = await fetch('https://your-app.com/api/erp/export-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, options: { syncInventory, syncCustomer, format: exportFormat[0] }, }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { const result = await response.json(); error = result.message || 'Failed to export order to ERP'; updateUI(); } } catch (err) { error = 'Connection error. Please check your network and try again.'; updateUI(); } finally { loading = false; updateUI(); } }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Export to ERP System', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that generates shipping labels by fetching order details using the [GraphQL Admin API](/docs/api/admin-graphql). This example demonstrates using query() to retrieve shipping address and package information for carrier API integration.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-details.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const [orderData, setOrderData] = useState(null); const [includeBarcode, setIncludeBarcode] = useState(true); const [includeGiftMessage, setIncludeGiftMessage] = useState(false); useEffect(() => { fetchOrderDetails(); }, []); const fetchOrderDetails = async () => { const orderId = data.selected[0].id; try { const result = await query( `query GetOrderForPackingSlip($id: ID!) { order(id: $id) { name createdAt shippingAddress { name address1 address2 city province zip country } lineItems(first: 50) { edges { node { title quantity sku variant { title } } } } customer { displayName email } } }`, { variables: { id: orderId } } ); if (result.data?.order) { setOrderData(result.data.order); } else { setError('Could not load order details'); } } catch (err) { setError('Failed to fetch order data'); } finally { setLoading(false); } }; const handleGenerate = async () => { setGenerating(true); try { const response = await fetch('https://your-app.com/api/packing-slip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order: orderData, options: { includeBarcode, includeGiftMessage } }), }); if (response.ok) { setSuccess(true); close(); } else { setError('Failed to generate packing slip'); } } catch (err) { setError('Generation failed'); } finally { setGenerating(false); } }; const itemCount = orderData?.lineItems?.edges?.length || 0; return ( <AdminAction title="Generate Packing Slip" primaryAction={ <Button onPress={handleGenerate} disabled={loading || generating || success || !orderData}> {generating ? 'Generating...' : 'Generate PDF'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Packing slip generated! Check your downloads.</Banner>} {error && <Banner tone="critical">{error}</Banner>} {loading ? ( <Banner tone="info">Loading order details...</Banner> ) : orderData && ( <> <Box padding="base"> <BlockStack gap="tight"> <Banner tone="info"> Order {orderData.name} • {itemCount} item{itemCount !== 1 ? 's' : ''} • Ship to: {orderData.shippingAddress?.city}, {orderData.shippingAddress?.country} </Banner> </BlockStack> </Box> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode}> Include barcode for scanning </Checkbox> <Checkbox checked={includeGiftMessage} onChange={setIncludeGiftMessage}> Include gift message section </Checkbox> </BlockStack> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.action.render', (root, api) => { let loading = true; let generating = false; let success = false; let error = ''; let orderData: any = null; let includeBarcode = true; let includeGiftMessage = false; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const itemCount = orderData?.lineItems?.edges?.length || 0; const buttonText = generating ? 'Generating...' : 'Generate PDF'; primaryAction.appendChild( root.createComponent(Button, { onPress: handleGenerate, disabled: loading || generating || success || !orderData }, buttonText) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Packing slip generated! Check your downloads.') ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } if (loading) { stack.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading order details...') ); } else if (orderData) { const infoBox = root.createComponent(Box, { padding: 'base' }); const infoStack = root.createComponent(BlockStack, { gap: 'tight' }); const infoText = `Order ${orderData.name} • ${itemCount} item${itemCount !== 1 ? 's' : ''} • Ship to: ${orderData.shippingAddress?.city}, ${orderData.shippingAddress?.country}`; infoStack.appendChild(root.createComponent(Banner, { tone: 'info' }, infoText)); infoBox.appendChild(infoStack); stack.appendChild(infoBox); stack.appendChild(root.createComponent(Divider, {})); const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent(Checkbox, { checked: includeBarcode, onChange: (val: boolean) => { includeBarcode = val; updateUI(); } }, 'Include barcode for scanning') ); optionsStack.appendChild( root.createComponent(Checkbox, { checked: includeGiftMessage, onChange: (val: boolean) => { includeGiftMessage = val; updateUI(); } }, 'Include gift message section') ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); } content.appendChild(stack); }; const fetchOrderDetails = async () => { const orderId = api.data.selected[0].id; try { const result = await api.query( `query GetOrderForPackingSlip($id: ID!) { order(id: $id) { name createdAt shippingAddress { name address1 address2 city province zip country } lineItems(first: 50) { edges { node { title quantity sku variant { title } } } } customer { displayName email } } }`, { variables: { id: orderId } } ); if (result.data?.order) { orderData = result.data.order; } else { error = 'Could not load order details'; } } catch (err) { error = 'Failed to fetch order data'; } finally { loading = false; updateUI(); } }; const handleGenerate = async () => { generating = true; updateUI(); try { const response = await fetch('https://your-app.com/api/packing-slip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order: orderData, options: { includeBarcode, includeGiftMessage } }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { error = 'Failed to generate packing slip'; } } catch (err) { error = 'Generation failed'; } finally { generating = false; updateUI(); } }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); fetchOrderDetails(); const adminAction = root.createComponent(AdminAction, { title: 'Generate Packing Slip', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
Anchor to Order details action (should render) ,[object Object]Order details action (should render) target
admin.order-details.action.should-render
Controls the render state of an admin action extension on the order details page. Use this target to conditionally show or hide your action extension based on the order's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add a should-render extension that checks your app backend to determine if an order uses your custom fulfillment service and has active tracking data available.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.action.should-render', async ({data}) => { const orderId = data.selected[0].id; const orderGid = orderId.split('/').pop(); try { const response = await fetch( `https://your-app.com/api/fulfillment/check?order_id=${orderGid}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, } ); if (!response.ok) { return {display: false}; } const result = await response.json(); // Show action if order uses our fulfillment service and has tracking const shouldDisplay = result.usesCustomFulfillment === true && result.hasActiveTracking === true && result.fulfillmentStatus !== 'delivered'; return {display: shouldDisplay}; } catch (err) { console.error('Failed to check fulfillment status:', err); return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.action.should-render', async ({data}) => { const orderId = data.selected[0].id; const orderGid = orderId.split('/').pop(); try { const response = await fetch( `https://your-app.com/api/fulfillment/check?order_id=${orderGid}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, } ); if (!response.ok) { return {display: false}; } const result = await response.json(); // Show action if order uses our fulfillment service and has tracking const shouldDisplay = result.usesCustomFulfillment === true && result.hasActiveTracking === true && result.fulfillmentStatus !== 'delivered'; return {display: shouldDisplay}; } catch (err) { console.error('Failed to check fulfillment status:', err); return {display: false}; } } );Description
Add a should-render extension that displays a fraud risk assessment action only for orders with medium or high risk levels. This example demonstrates querying the GraphQL Admin API to check order risk assessments.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.action.should-render', async ({data, query}) => { const orderId = data.selected[0].id; try { const {data: responseData, errors} = await query<{ order: { risk: { assessments: Array<{ riskLevel: 'LOW' | 'MEDIUM' | 'HIGH'; provider: { title: string }; }>; }; }; }>(` query GetOrderRisk($id: ID!) { order(id: $id) { risk { assessments { riskLevel provider { title } } } } } `, { variables: { id: orderId } }); if (errors || !responseData?.order?.risk?.assessments) { return { display: false }; } const assessments = responseData.order.risk.assessments; // Show action if any assessment indicates medium or high risk const hasElevatedRisk = assessments.some( (assessment) => assessment.riskLevel === 'MEDIUM' || assessment.riskLevel === 'HIGH' ); return { display: hasElevatedRisk }; } catch (err) { console.error('Failed to fetch order risk:', err); return { display: false }; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.action.should-render', async ({data, query}) => { const orderId = data.selected[0].id; try { const {data: responseData, errors} = await query<{ order: { risk: { assessments: Array<{ riskLevel: 'LOW' | 'MEDIUM' | 'HIGH'; provider: { title: string }; }>; }; }; }>(` query GetOrderRisk($id: ID!) { order(id: $id) { risk { assessments { riskLevel provider { title } } } } } `, { variables: { id: orderId } }); if (errors || !responseData?.order?.risk?.assessments) { return { display: false }; } const assessments = responseData.order.risk.assessments; // Show action if any assessment indicates medium or high risk const hasElevatedRisk = assessments.some( (assessment) => assessment.riskLevel === 'MEDIUM' || assessment.riskLevel === 'HIGH' ); return { display: hasElevatedRisk }; } catch (err) { console.error('Failed to fetch order risk:', err); return { display: false }; } } );
Anchor to Order details block ,[object Object]Order details block target
admin.order-details.block.render
Renders an admin block extension inline on the order details page. Use this target to display contextual information, analytics, or status updates related to the order without requiring merchant interaction to open a modal.
Extensions at this target can access order data through the data property in the Block Extension API. Blocks appear as cards on the page and can show real-time data, insights, or quick actions. They provide persistent visibility for information merchants need to see at a glance.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Create a block extension that shows fulfillment progress by fetching status updates from your app backend. This example demonstrates how to display shipping milestones and carrier tracking information.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminBlock, Banner, BlockStack, Box, Button, Divider, Heading, Icon, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-details.block.render'; export default reactExtension(TARGET, () => <App />); function App() { const {data, query} = useApi(TARGET); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchFulfillmentStatus = async () => { setLoading(true); setError(null); try { const orderData = await query(`query GetOrder($id: ID!) { order(id: $id) { name } }`, { variables: { id: data.selected[0].id } }); const response = await fetch('https://your-app.com/api/fulfillment/status', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId: data.selected[0].id }), }); if (!response.ok) throw new Error('Failed to fetch status'); const result = await response.json(); setStatus(result); } catch (err) { setError('Unable to load fulfillment status'); } finally { setLoading(false); } }; useEffect(() => { fetchFulfillmentStatus(); }, []); const getMilestoneIcon = (completed) => completed ? 'CheckCircleFill' : 'CircleFill'; return ( <AdminBlock title="Fulfillment Status"> {error && <Banner tone="critical">{error}</Banner>} {loading ? ( <BlockStack> <Box padding="base">Loading fulfillment data...</Box> </BlockStack> ) : status ? ( <BlockStack gap="base"> <Banner tone={status.delivered ? 'success' : 'info'}> {status.delivered ? 'Order delivered!' : `In transit: ${status.carrier}`} </Banner> <Divider /> <BlockStack gap="tight"> <Heading size="6">Tracking Milestones</Heading> {status.milestones?.map((milestone, idx) => ( <BlockStack key={idx} gap="extraTight"> <Box inlineAlignment="start"> <Icon name={getMilestoneIcon(milestone.completed)} /> {` ${milestone.label} - ${milestone.timestamp || 'Pending'}`} </Box> </BlockStack> ))} </BlockStack> <Divider /> <Button onPress={fetchFulfillmentStatus}>Refresh Status</Button> </BlockStack> ) : ( <Box padding="base">No fulfillment data available</Box> )} </AdminBlock> ); }TS
import { extension, AdminBlock, Banner, BlockStack, Box, Button, Divider, Heading, Icon, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.block.render', (root, api) => { let status = null; let loading = true; let error = null; const content = root.createFragment(); const fetchFulfillmentStatus = async () => { loading = true; error = null; updateUI(); try { await api.query(`query GetOrder($id: ID!) { order(id: $id) { name } }`, { variables: { id: api.data.selected[0].id } }); const response = await fetch('https://your-app.com/api/fulfillment/status', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId: api.data.selected[0].id }), }); if (!response.ok) throw new Error('Failed to fetch status'); status = await response.json(); } catch (err) { error = 'Unable to load fulfillment status'; } finally { loading = false; updateUI(); } }; const updateUI = () => { content.replaceChildren(); if (error) { content.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } if (loading) { const loadingStack = root.createComponent(BlockStack, {}); loadingStack.appendChild( root.createComponent(Box, { padding: 'base' }, 'Loading fulfillment data...') ); content.appendChild(loadingStack); } else if (status) { const mainStack = root.createComponent(BlockStack, { gap: 'base' }); mainStack.appendChild( root.createComponent( Banner, { tone: status.delivered ? 'success' : 'info' }, status.delivered ? 'Order delivered!' : `In transit: ${status.carrier}` ) ); mainStack.appendChild(root.createComponent(Divider, {})); const milestonesStack = root.createComponent(BlockStack, { gap: 'tight' }); milestonesStack.appendChild( root.createComponent(Heading, { size: '6' }, 'Tracking Milestones') ); status.milestones?.forEach((milestone) => { const milestoneBox = root.createComponent(Box, { inlineAlignment: 'start' }); milestoneBox.appendChild( root.createComponent(Icon, { name: milestone.completed ? 'CheckCircleFill' : 'CircleFill' }) ); milestoneBox.appendChild(` ${milestone.label} - ${milestone.timestamp || 'Pending'}`); milestonesStack.appendChild(milestoneBox); }); mainStack.appendChild(milestonesStack); mainStack.appendChild(root.createComponent(Divider, {})); mainStack.appendChild( root.createComponent(Button, { onPress: fetchFulfillmentStatus }, 'Refresh Status') ); content.appendChild(mainStack); } else { content.appendChild( root.createComponent(Box, { padding: 'base' }, 'No fulfillment data available') ); } }; const adminBlock = root.createComponent(AdminBlock, { title: 'Fulfillment Status', }); adminBlock.appendChild(content); root.appendChild(adminBlock); fetchFulfillmentStatus(); root.mount(); } );Description
Create a block extension that shows fraud risk scoring for an order. This example demonstrates using the GraphQL Admin API to query risk data and present it with visual indicators.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminBlock, Banner, BlockStack, Box, Divider, Heading, Icon, Button, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-details.block.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, query } = useApi(TARGET); const [riskData, setRiskData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchRiskData = async () => { setLoading(true); setError(null); const orderId = data.selected[0].id; try { const result = await query(` query GetOrderRisk($id: ID!) { order(id: $id) { id name riskLevel risks { level message } totalPriceSet { shopMoney { amount currencyCode } } } } `, { variables: { id: orderId } }); if (result.data?.order) { setRiskData(result.data.order); } else { setError('Unable to fetch risk data'); } } catch (err) { setError('Failed to load risk assessment'); } finally { setLoading(false); } }; useEffect(() => { fetchRiskData(); }, []); const getRiskTone = (level) => { switch (level) { case 'HIGH': return 'critical'; case 'MEDIUM': return 'warning'; default: return 'success'; } }; if (loading) { return ( <AdminBlock title="Fraud Risk Assessment"> <BlockStack> <Banner tone="info">Loading risk assessment...</Banner> </BlockStack> </AdminBlock> ); } if (error) { return ( <AdminBlock title="Fraud Risk Assessment"> <BlockStack> <Banner tone="critical">{error}</Banner> <Button onPress={fetchRiskData}>Retry</Button> </BlockStack> </AdminBlock> ); } return ( <AdminBlock title="Fraud Risk Assessment"> <BlockStack gap="base"> <Box padding="base"> <BlockStack gap="tight"> <Heading>Risk Level: {riskData.riskLevel || 'LOW'}</Heading> <Banner tone={getRiskTone(riskData.riskLevel)}> Order {riskData.name} - {riskData.totalPriceSet?.shopMoney?.amount} {riskData.totalPriceSet?.shopMoney?.currencyCode} </Banner> </BlockStack> </Box> <Divider /> <Box padding="base"> <BlockStack gap="tight"> <Heading>Risk Indicators</Heading> {riskData.risks?.length > 0 ? ( riskData.risks.map((risk, index) => ( <Banner key={index} tone={getRiskTone(risk.level)}> {risk.message} </Banner> )) ) : ( <Banner tone="success">No risk indicators detected</Banner> )} </BlockStack> </Box> </BlockStack> </AdminBlock> ); }TS
import { extension, AdminBlock, Banner, BlockStack, Box, Divider, Heading, Button, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.block.render', (root, api) => { const content = root.createFragment(); let riskData = null; const getRiskTone = (level) => { switch (level) { case 'HIGH': return 'critical'; case 'MEDIUM': return 'warning'; default: return 'success'; } }; const renderLoading = () => { content.replaceChildren(); const stack = root.createComponent(BlockStack, {}); stack.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading risk assessment...') ); content.appendChild(stack); }; const renderError = (message) => { content.replaceChildren(); const stack = root.createComponent(BlockStack, {}); stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, message)); stack.appendChild( root.createComponent(Button, { onPress: fetchRiskData }, 'Retry') ); content.appendChild(stack); }; const renderRiskData = () => { content.replaceChildren(); const mainStack = root.createComponent(BlockStack, { gap: 'base' }); const headerBox = root.createComponent(Box, { padding: 'base' }); const headerStack = root.createComponent(BlockStack, { gap: 'tight' }); headerStack.appendChild( root.createComponent(Heading, {}, `Risk Level: ${riskData.riskLevel || 'LOW'}`) ); headerStack.appendChild( root.createComponent( Banner, { tone: getRiskTone(riskData.riskLevel) }, `Order ${riskData.name} - ${riskData.totalPriceSet?.shopMoney?.amount} ${riskData.totalPriceSet?.shopMoney?.currencyCode}` ) ); headerBox.appendChild(headerStack); mainStack.appendChild(headerBox); mainStack.appendChild(root.createComponent(Divider, {})); const indicatorsBox = root.createComponent(Box, { padding: 'base' }); const indicatorsStack = root.createComponent(BlockStack, { gap: 'tight' }); indicatorsStack.appendChild(root.createComponent(Heading, {}, 'Risk Indicators')); if (riskData.risks?.length > 0) { riskData.risks.forEach((risk) => { indicatorsStack.appendChild( root.createComponent(Banner, { tone: getRiskTone(risk.level) }, risk.message) ); }); } else { indicatorsStack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'No risk indicators detected') ); } indicatorsBox.appendChild(indicatorsStack); mainStack.appendChild(indicatorsBox); content.appendChild(mainStack); }; const fetchRiskData = async () => { renderLoading(); const orderId = api.data.selected[0].id; try { const result = await api.query(` query GetOrderRisk($id: ID!) { order(id: $id) { id name riskLevel risks { level message } totalPriceSet { shopMoney { amount currencyCode } } } } `, { variables: { id: orderId } }); if (result.data?.order) { riskData = result.data.order; renderRiskData(); } else { renderError('Unable to fetch risk data'); } } catch (err) { renderError('Failed to load risk assessment'); } }; const adminBlock = root.createComponent(AdminBlock, { title: 'Fraud Risk Assessment', }); adminBlock.appendChild(content); root.appendChild(adminBlock); fetchRiskData(); root.mount(); } );
Anchor to Order details print action ,[object Object]Order details print action target
admin.order-details.print-action.render
Renders an admin print action extension on the order details page. Merchants access this extension from the Print menu in the toolbar. Use this target to provide custom print workflows such as generating packing slips, shipping labels, invoices, or other order-related documents. The extension contains special APIs to display a preview of the document and send it to a printer.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add a print action extension that sends order details to your ERP system to generate a custom packing slip. This example demonstrates calling an app backend with order data and providing print format options.
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-details.print-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [format, setFormat] = useState(['standard']); const [includeBarcode, setIncludeBarcode] = useState(true); const [includePricing, setIncludePricing] = useState(false); const handleGenerateSlip = async () => { setLoading(true); setError(null); const orderId = data.selected[0].id; try { const response = await fetch('https://your-app.com/api/erp/packing-slip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, format: format[0], includeBarcode, includePricing, }), }); if (response.ok) { const result = await response.json(); setSuccess(true); close(); } else { const errorData = await response.json(); setError(errorData.message || 'Failed to generate packing slip'); } } catch (err) { setError('Connection to ERP failed. Please try again.'); } finally { setLoading(false); } }; return ( <AdminAction title="Generate ERP Packing Slip" primaryAction={ <Button onPress={handleGenerateSlip} disabled={loading || success}> {loading ? 'Generating...' : 'Generate & Print'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success"> Packing slip sent to ERP. Print job queued successfully. </Banner> )} {error && <Banner tone="critical">{error}</Banner>} <Box padding="base"> <BlockStack gap="base"> <ChoiceList name="format" title="Slip Format" value={format} onChange={setFormat} choices={[ { label: 'Standard (Letter)', value: 'standard' }, { label: 'Compact (4x6)', value: 'compact' }, { label: 'Detailed (with notes)', value: 'detailed' }, ]} /> <Divider /> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode} > Include scannable barcode </Checkbox> <Checkbox checked={includePricing} onChange={setIncludePricing} > Include item pricing </Checkbox> </BlockStack> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.print-action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let format = ['standard']; let includeBarcode = true; let includePricing = false; const handleGenerateSlip = async () => { loading = true; error = null; updateUI(); const orderId = api.data.selected[0].id; try { const response = await fetch('https://your-app.com/api/erp/packing-slip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, format: format[0], includeBarcode, includePricing, }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { const errorData = await response.json(); error = errorData.message || 'Failed to generate packing slip'; updateUI(); } } catch (err) { error = 'Connection to ERP failed. Please try again.'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handleGenerateSlip, disabled: loading || success }, loading ? 'Generating...' : 'Generate & Print' ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Packing slip sent to ERP. Print job queued successfully.' ) ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'format', title: 'Slip Format', value: format, onChange: (val: string[]) => { format = val; updateUI(); }, choices: [ { label: 'Standard (Letter)', value: 'standard' }, { label: 'Compact (4x6)', value: 'compact' }, { label: 'Detailed (with notes)', value: 'detailed' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent(Checkbox, { checked: includeBarcode, onChange: (val: boolean) => { includeBarcode = val; updateUI(); }, }, 'Include scannable barcode') ); innerStack.appendChild( root.createComponent(Checkbox, { checked: includePricing, onChange: (val: boolean) => { includePricing = val; updateUI(); }, }, 'Include item pricing') ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Generate ERP Packing Slip', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add a print action extension that generates customer invoices by fetching order details using the GraphQL Admin API. This example demonstrates using query() to retrieve line items, pricing, and tax information for invoice generation.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-details.print-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [orderData, setOrderData] = useState(null); const [error, setError] = useState(null); const [includeBarcode, setIncludeBarcode] = useState(true); const [includePrices, setIncludePrices] = useState(false); const [includeNotes, setIncludeNotes] = useState(true); useEffect(() => { fetchOrderDetails(); }, []); const fetchOrderDetails = async () => { const orderId = data.selected[0].id; try { const result = await query( `query GetOrder($id: ID!) { order(id: $id) { name createdAt shippingAddress { name address1 address2 city province zip country } lineItems(first: 50) { edges { node { title quantity sku variant { title } } } } note } }`, { variables: { id: orderId } } ); setOrderData(result.data.order); } catch (err) { setError('Failed to load order details'); } finally { setLoading(false); } }; const handlePrint = () => { const printData = { order: orderData, options: { includeBarcode, includePrices, includeNotes } }; console.log('Generating packing slip:', printData); window.print(); close(); }; const itemCount = orderData?.lineItems?.edges?.length || 0; return ( <AdminAction title="Generate Packing Slip" primaryAction={ <Button onPress={handlePrint} disabled={loading || !!error}> Print Packing Slip </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {error && <Banner tone="critical">{error}</Banner>} {loading && <Banner tone="info">Loading order details...</Banner>} {orderData && ( <> <Banner tone="success"> Order {orderData.name} - {itemCount} item(s) ready for packing slip </Banner> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode} > Include barcode for scanning </Checkbox> <Checkbox checked={includePrices} onChange={setIncludePrices} > Include item prices </Checkbox> <Checkbox checked={includeNotes} onChange={setIncludeNotes} > Include order notes </Checkbox> </BlockStack> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.print-action.render', async (root, api) => { let orderData = null; let error = null; let includeBarcode = true; let includePrices = false; let includeNotes = true; const orderId = api.data.selected[0].id; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); if (error) { content.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } if (orderData) { const itemCount = orderData.lineItems?.edges?.length || 0; content.appendChild( root.createComponent(BlockStack, { gap: 'base' }, root.createComponent(Banner, { tone: 'success' }, `Order ${orderData.name} - ${itemCount} item(s) ready for packing slip` ), root.createComponent(Divider, {}), root.createComponent(Box, { padding: 'base' }, root.createComponent(BlockStack, { gap: 'base' }, root.createComponent(Checkbox, { checked: includeBarcode, onChange: (val) => { includeBarcode = val; updateUI(); } }, 'Include barcode for scanning'), root.createComponent(Checkbox, { checked: includePrices, onChange: (val) => { includePrices = val; updateUI(); } }, 'Include item prices'), root.createComponent(Checkbox, { checked: includeNotes, onChange: (val) => { includeNotes = val; updateUI(); } }, 'Include order notes') ) ) ) ); } }; const handlePrint = () => { console.log('Generating packing slip:', { order: orderData, options: { includeBarcode, includePrices, includeNotes } }); api.close(); }; primaryAction.appendChild( root.createComponent(Button, { onPress: handlePrint, disabled: !orderData }, 'Print Packing Slip') ); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); const adminAction = root.createComponent(AdminAction, { title: 'Generate Packing Slip', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); content.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading order details...') ); root.mount(); try { const result = await api.query( `query GetOrder($id: ID!) { order(id: $id) { name createdAt shippingAddress { name address1 city province zip country } lineItems(first: 50) { edges { node { title quantity sku variant { title } } } } note } }`, { variables: { id: orderId } } ); orderData = result.data.order; } catch (err) { error = 'Failed to load order details'; } updateUI(); } );
Anchor to Order details print action (should render) ,[object Object]Order details print action (should render) target
admin.order-details.print-action.should-render
Controls the render state of an admin action extension on the order details page. Use this target to conditionally show or hide your action extension based on the order's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Conditionally display a print action only for orders that contain physical products requiring shipment. This example demonstrates checking order line items to determine if printing is applicable.
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-details.print-action.should-render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [exportType, setExportType] = useState(['full']); const [includeCustomer, setIncludeCustomer] = useState(true); const [includeShipping, setIncludeShipping] = useState(true); const handleExport = async () => { setLoading(true); setError(null); const orderId = data.selected[0].id; try { const response = await fetch('https://your-app.com/api/erp/export-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, exportType: exportType[0], includeCustomer, includeShipping, }), }); if (response.ok) { setSuccess(true); close(); } else { const result = await response.json(); setError(result.message || 'Failed to export order to ERP'); } } catch (err) { setError('Connection error. Please check your network and try again.'); } finally { setLoading(false); } }; return ( <AdminAction title="Export Order to ERP" primaryAction={ <Button onPress={handleExport} disabled={loading || success}> {loading ? 'Exporting...' : 'Export to ERP'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success">Order successfully exported to ERP system!</Banner> )} {error && <Banner tone="critical">{error}</Banner>} <Box> <ChoiceList name="exportType" title="Export Type" choices={[ { id: 'full', label: 'Full order data' }, { id: 'financial', label: 'Financial data only' }, { id: 'inventory', label: 'Inventory update only' }, ]} value={exportType} onChange={setExportType} /> </Box> <Divider /> <BlockStack gap="base"> <Checkbox checked={includeCustomer} onChange={setIncludeCustomer} > Include customer information </Checkbox> <Checkbox checked={includeShipping} onChange={setIncludeShipping} > Include shipping details </Checkbox> </BlockStack> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.print-action.should-render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let exportType = ['full']; let includeCustomer = true; let includeShipping = true; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); if (success) { content.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Order successfully exported to ERP system!') ); } if (error) { content.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const stack = root.createComponent(BlockStack, { gap: 'base' }); const choiceBox = root.createComponent(Box, {}); choiceBox.appendChild( root.createComponent(ChoiceList, { name: 'exportType', title: 'Export Type', choices: [ { id: 'full', label: 'Full order data' }, { id: 'financial', label: 'Financial data only' }, { id: 'inventory', label: 'Inventory update only' }, ], value: exportType, onChange: (val) => { exportType = val; updateUI(); }, }) ); stack.appendChild(choiceBox); stack.appendChild(root.createComponent(Divider, {})); const checkboxStack = root.createComponent(BlockStack, { gap: 'base' }); checkboxStack.appendChild( root.createComponent(Checkbox, { checked: includeCustomer, onChange: (val) => { includeCustomer = val; updateUI(); }, }, 'Include customer information') ); checkboxStack.appendChild( root.createComponent(Checkbox, { checked: includeShipping, onChange: (val) => { includeShipping = val; updateUI(); }, }, 'Include shipping details') ); stack.appendChild(checkboxStack); content.appendChild(stack); }; const handleExport = async () => { loading = true; error = null; updatePrimaryButton(); const orderId = api.data.selected[0].id; try { const response = await fetch('https://your-app.com/api/erp/export-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, exportType: exportType[0], includeCustomer, includeShipping, }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { const result = await response.json(); error = result.message || 'Failed to export order to ERP'; updateUI(); } } catch (err) { error = 'Connection error. Please check your network and try again.'; updateUI(); } finally { loading = false; updatePrimaryButton(); } }; const updatePrimaryButton = () => { primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent(Button, { onPress: handleExport, disabled: loading || success, }, loading ? 'Exporting...' : 'Export to ERP') ); }; updatePrimaryButton(); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Export Order to ERP', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Conditionally display a print action only when the merchant has configured their print settings. This example demonstrates checking app configuration before showing the extension.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-details.print-action.should-render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(false); const [orderData, setOrderData] = useState(null); const [error, setError] = useState(''); const [includeBarcode, setIncludeBarcode] = useState(true); const [includePrices, setIncludePrices] = useState(false); const [copies, setCopies] = useState(['1']); useEffect(() => { fetchOrderDetails(); }, []); const fetchOrderDetails = async () => { const orderId = data.selected[0].id; try { const result = await query(` query GetOrderForPackingSlip($id: ID!) { order(id: $id) { name createdAt shippingAddress { name address1 address2 city province zip country } lineItems(first: 50) { edges { node { title quantity sku variant { title } } } } fulfillments(first: 5) { trackingInfo { number company } } } } `, { variables: { id: orderId } }); if (result.data?.order) { setOrderData(result.data.order); } else { setError('Could not load order details'); } } catch (err) { setError('Failed to fetch order data'); } finally { setLoading(false); } }; const handleGenerate = async () => { setGenerating(true); // Simulate PDF generation await new Promise(resolve => setTimeout(resolve, 1500)); setGenerating(false); close(); }; const itemCount = orderData?.lineItems?.edges?.length || 0; return ( <AdminAction title="Generate Packing Slip" primaryAction={ <Button onPress={handleGenerate} disabled={loading || generating || !!error}> {generating ? 'Generating...' : 'Generate PDF'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {error && <Banner tone="critical">{error}</Banner>} {loading ? ( <Banner tone="info">Loading order details...</Banner> ) : orderData && ( <> <Banner tone="info"> Order {orderData.name} • {itemCount} item{itemCount !== 1 ? 's' : ''} to pack </Banner> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode} > Include barcode for scanning </Checkbox> <Checkbox checked={includePrices} onChange={setIncludePrices} > Show item prices on slip </Checkbox> </BlockStack> </Box> <Divider /> <ChoiceList title="Number of copies" name="copies" value={copies} onChange={setCopies} choices={[ { label: '1 copy', value: '1' }, { label: '2 copies', value: '2' }, { label: '3 copies', value: '3' }, ]} /> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-details.print-action.should-render', (root, api) => { let loading = true; let generating = false; let orderData: any = null; let error = ''; let includeBarcode = true; let includePrices = false; let copies = ['1']; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const fetchOrderDetails = async () => { const orderId = api.data.selected[0].id; try { const result = await api.query(` query GetOrderForPackingSlip($id: ID!) { order(id: $id) { name createdAt shippingAddress { name address1 city province zip country } lineItems(first: 50) { edges { node { title quantity sku variant { title } } } } fulfillments(first: 5) { trackingInfo { number company } } } } `, { variables: { id: orderId } }); if (result.data?.order) { orderData = result.data.order; } else { error = 'Could not load order details'; } } catch (err) { error = 'Failed to fetch order data'; } finally { loading = false; updateUI(); } }; const handleGenerate = async () => { generating = true; updateUI(); await new Promise(resolve => setTimeout(resolve, 1500)); api.close(); }; const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const itemCount = orderData?.lineItems?.edges?.length || 0; primaryAction.appendChild( root.createComponent(Button, { onPress: handleGenerate, disabled: loading || generating || !!error, }, generating ? 'Generating...' : 'Generate PDF') ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } else if (loading) { stack.appendChild(root.createComponent(Banner, { tone: 'info' }, 'Loading order details...')); } else if (orderData) { stack.appendChild(root.createComponent(Banner, { tone: 'info' }, `Order ${orderData.name} • ${itemCount} item${itemCount !== 1 ? 's' : ''} to pack` )); stack.appendChild(root.createComponent(Divider)); const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild(root.createComponent(Checkbox, { checked: includeBarcode, onChange: (val: boolean) => { includeBarcode = val; updateUI(); }, }, 'Include barcode for scanning')); optionsStack.appendChild(root.createComponent(Checkbox, { checked: includePrices, onChange: (val: boolean) => { includePrices = val; updateUI(); }, }, 'Show item prices on slip')); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); stack.appendChild(root.createComponent(Divider)); stack.appendChild(root.createComponent(ChoiceList, { title: 'Number of copies', name: 'copies', value: copies, onChange: (val: string[]) => { copies = val; updateUI(); }, choices: [ { label: '1 copy', value: '1' }, { label: '2 copies', value: '2' }, { label: '3 copies', value: '3' }, ], })); } content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); fetchOrderDetails(); const adminAction = root.createComponent(AdminAction, { title: 'Generate Packing Slip', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
Anchor to Order index targetsOrder index targets
Use action targets to extend the order index page with workflows and operations.
Anchor to Order index action ,[object Object]Order index action target
admin.order-index.action.render
Renders an admin action extension on the order index page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on order data, such as syncing with external systems, exporting order information, or managing credit terms.
Extensions at this target can access order data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that initiates a full order sync to an external ERP system. This example demonstrates bulk order selection, backend API integration, and progress feedback for multi-order operations.
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-index.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [exportFormat, setExportFormat] = useState(['full']); const [includeLineItems, setIncludeLineItems] = useState(true); const [includeCustomer, setIncludeCustomer] = useState(true); const selectedOrders = data.selected; const orderCount = selectedOrders.length; const handleExport = async () => { setLoading(true); setError(null); try { const orderIds = selectedOrders.map(order => order.id); const response = await fetch('https://your-app.com/api/erp/export-orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds, format: exportFormat[0], includeLineItems, includeCustomer, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Export failed'); } setSuccess(true); close(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to export orders'); } finally { setLoading(false); } }; return ( <AdminAction title="Export to ERP" primaryAction={ <Button onPress={handleExport} disabled={loading || success || orderCount === 0}> {loading ? 'Exporting...' : `Export ${orderCount} Order${orderCount !== 1 ? 's' : ''}`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success"> Successfully exported {orderCount} order{orderCount !== 1 ? 's' : ''} to ERP </Banner> )} {error && <Banner tone="critical">{error}</Banner>} {orderCount === 0 && ( <Banner tone="warning">Please select at least one order to export</Banner> )} <Box padding="base"> <ChoiceList title="Export Format" name="exportFormat" choices={[ { id: 'full', label: 'Full order data' }, { id: 'summary', label: 'Summary only' }, { id: 'financial', label: 'Financial data only' }, ]} value={exportFormat} onChange={setExportFormat} /> </Box> <Divider /> <BlockStack gap="base"> <Checkbox checked={includeLineItems} onChange={setIncludeLineItems} > Include line item details </Checkbox> <Checkbox checked={includeCustomer} onChange={setIncludeCustomer} > Include customer information </Checkbox> </BlockStack> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let exportFormat = ['full']; let includeLineItems = true; let includeCustomer = true; const selectedOrders = api.data.selected; const orderCount = selectedOrders.length; const handleExport = async () => { loading = true; error = null; updateUI(); try { const orderIds = selectedOrders.map(order => order.id); const response = await fetch('https://your-app.com/api/erp/export-orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds, format: exportFormat[0], includeLineItems, includeCustomer, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Export failed'); } success = true; updateUI(); api.close(); } catch (err) { error = err instanceof Error ? err.message : 'Failed to export orders'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createComponent(BlockStack, { gap: 'base' }); const updateUI = () => { primaryAction.replaceChildren( root.createComponent( Button, { onPress: handleExport, disabled: loading || success || orderCount === 0 }, loading ? 'Exporting...' : `Export ${orderCount} Order${orderCount !== 1 ? 's' : ''}` ) ); content.replaceChildren(); if (success) { content.appendChild( root.createComponent(Banner, { tone: 'success' }, `Successfully exported ${orderCount} order${orderCount !== 1 ? 's' : ''} to ERP` ) ); } if (error) { content.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (orderCount === 0) { content.appendChild( root.createComponent(Banner, { tone: 'warning' }, 'Please select at least one order to export') ); } const formatBox = root.createComponent(Box, { padding: 'base' }); formatBox.appendChild( root.createComponent(ChoiceList, { title: 'Export Format', name: 'exportFormat', choices: [ { id: 'full', label: 'Full order data' }, { id: 'summary', label: 'Summary only' }, { id: 'financial', label: 'Financial data only' }, ], value: exportFormat, onChange: (val: string[]) => { exportFormat = val; updateUI(); }, }) ); content.appendChild(formatBox); content.appendChild(root.createComponent(Divider, {})); const checkboxStack = root.createComponent(BlockStack, { gap: 'base' }); checkboxStack.appendChild( root.createComponent(Checkbox, { checked: includeLineItems, onChange: (val: boolean) => { includeLineItems = val; updateUI(); }, }, 'Include line item details') ); checkboxStack.appendChild( root.createComponent(Checkbox, { checked: includeCustomer, onChange: (val: boolean) => { includeCustomer = val; updateUI(); }, }, 'Include customer information') ); content.appendChild(checkboxStack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Export to ERP', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that creates an order fulfillment summary report. This example demonstrates aggregating order data to generate insights about fulfillment status and shipping performance.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-index.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const [orderData, setOrderData] = useState(null); const [includeBarcode, setIncludeBarcode] = useState(true); const [includeGiftMessage, setIncludeGiftMessage] = useState(false); useEffect(() => { fetchOrderDetails(); }, []); const fetchOrderDetails = async () => { const orderId = data.selected[0].id; try { const result = await query( `query GetOrderForPackingSlip($id: ID!) { order(id: $id) { name createdAt shippingAddress { name address1 city provinceCode zip country } lineItems(first: 20) { edges { node { title quantity sku } } } } }`, { variables: { id: orderId } } ); if (result.data?.order) { setOrderData(result.data.order); } else { setError('Could not load order details'); } } catch (err) { setError('Failed to fetch order data'); } finally { setLoading(false); } }; const handleGenerate = async () => { setGenerating(true); try { // Simulate PDF generation await new Promise(resolve => setTimeout(resolve, 1500)); setSuccess(true); close(); } catch (err) { setError('Failed to generate packing slip'); } finally { setGenerating(false); } }; const itemCount = orderData?.lineItems?.edges?.length || 0; return ( <AdminAction title="Generate Packing Slip" primaryAction={ <Button onPress={handleGenerate} disabled={loading || generating || success || !orderData}> {generating ? 'Generating...' : 'Generate PDF'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Packing slip generated successfully!</Banner>} {error && <Banner tone="critical">{error}</Banner>} {loading ? ( <Banner tone="info">Loading order details...</Banner> ) : orderData && ( <> <Box padding="base"> <BlockStack gap="tight"> <Banner tone="info"> Order {orderData.name} • {itemCount} item{itemCount !== 1 ? 's' : ''} </Banner> </BlockStack> </Box> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode} > Include barcode for scanning </Checkbox> <Checkbox checked={includeGiftMessage} onChange={setIncludeGiftMessage} > Include gift message section </Checkbox> </BlockStack> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.action.render', async (root, api) => { let loading = true; let generating = false; let success = false; let error = ''; let orderData: any = null; let includeBarcode = true; let includeGiftMessage = false; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const itemCount = orderData?.lineItems?.edges?.length || 0; const buttonText = generating ? 'Generating...' : 'Generate PDF'; primaryAction.appendChild( root.createComponent(Button, { onPress: handleGenerate, disabled: loading || generating || success || !orderData, }, buttonText) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Packing slip generated successfully!') ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } if (loading) { stack.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading order details...') ); } else if (orderData) { const infoBox = root.createComponent(Box, { padding: 'base' }); const infoStack = root.createComponent(BlockStack, { gap: 'tight' }); infoStack.appendChild( root.createComponent(Banner, { tone: 'info' }, `Order ${orderData.name} • ${itemCount} item${itemCount !== 1 ? 's' : ''}`) ); infoBox.appendChild(infoStack); stack.appendChild(infoBox); stack.appendChild(root.createComponent(Divider, {})); const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent(Checkbox, { checked: includeBarcode, onChange: (val) => { includeBarcode = val; updateUI(); }, }, 'Include barcode for scanning') ); optionsStack.appendChild( root.createComponent(Checkbox, { checked: includeGiftMessage, onChange: (val) => { includeGiftMessage = val; updateUI(); }, }, 'Include gift message section') ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); } content.appendChild(stack); }; const handleGenerate = async () => { generating = true; updateUI(); try { await new Promise(resolve => setTimeout(resolve, 1500)); success = true; updateUI(); api.close(); } catch (err) { error = 'Failed to generate packing slip'; updateUI(); } finally { generating = false; updateUI(); } }; const fetchOrderDetails = async () => { const orderId = api.data.selected[0].id; try { const result = await api.query( `query GetOrderForPackingSlip($id: ID!) { order(id: $id) { name createdAt shippingAddress { name address1 city provinceCode zip country } lineItems(first: 20) { edges { node { title quantity sku } } } } }`, { variables: { id: orderId } } ); if (result.data?.order) { orderData = result.data.order; } else { error = 'Could not load order details'; } } catch (err) { error = 'Failed to fetch order data'; } finally { loading = false; updateUI(); } }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Generate Packing Slip', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); await fetchOrderDetails(); } );
Anchor to Order index action (should render) ,[object Object]Order index action (should render) target
admin.order-index.action.should-render
Controls the render state of an admin action extension on the order index page. Use this target to conditionally show or hide your action extension based on the order's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add a should-render extension that checks with your app backend whether selected orders have active fulfillment tracking available, only displaying the action for orders that are pending fulfillment and registered in your logistics system.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.action.should-render', async ({data}) => { const selectedOrderIds = data.selected.map((item) => item.id); if (selectedOrderIds.length === 0) { return {display: false}; } try { // Call app backend to check fulfillment tracking availability const response = await fetch('https://your-app.com/api/fulfillment/check-tracking', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ orderIds: selectedOrderIds, }), }); if (!response.ok) { return {display: false}; } const result = await response.json(); // Show action if any selected orders have pending fulfillment // and are registered in the logistics tracking system const hasTrackableOrders = result.orders.some( (order: {hasPendingFulfillment: boolean; isRegisteredInLogistics: boolean}) => order.hasPendingFulfillment && order.isRegisteredInLogistics ); return {display: hasTrackableOrders}; } catch (err) { // Hide action if backend check fails return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.action.should-render', async ({data}) => { const selectedOrderIds = data.selected.map((item) => item.id); if (selectedOrderIds.length === 0) { return {display: false}; } try { // Call app backend to check fulfillment tracking availability const response = await fetch('https://your-app.com/api/fulfillment/check-tracking', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ orderIds: selectedOrderIds, }), }); if (!response.ok) { return {display: false}; } const result = await response.json(); // Show action if any selected orders have pending fulfillment // and are registered in the logistics tracking system const hasTrackableOrders = result.orders.some( (order: {hasPendingFulfillment: boolean; isRegisteredInLogistics: boolean}) => order.hasPendingFulfillment && order.isRegisteredInLogistics ); return {display: hasTrackableOrders}; } catch (err) { // Hide action if backend check fails return {display: false}; } } );Description
Add a should-render extension that checks order risk level using the Shopify API and only displays the action for orders flagged as high or medium risk, enabling targeted fraud review workflows.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.action.should-render', async ({data, query}) => { // Need at least one order selected if (!data.selected || data.selected.length === 0) { return {display: false}; } const orderId = data.selected[0].id; try { const {data: responseData, errors} = await query( `query GetOrderRisk($id: ID!) { order(id: $id) { id risk { assessments { riskLevel provider { title } } } } }`, {variables: {id: orderId}} ); if (errors || !responseData?.order?.risk?.assessments) { return {display: false}; } const assessments = responseData.order.risk.assessments; // Check if any assessment indicates high or medium risk const hasElevatedRisk = assessments.some( (assessment: {riskLevel: string}) => assessment.riskLevel === 'HIGH' || assessment.riskLevel === 'MEDIUM' ); return {display: hasElevatedRisk}; } catch (err) { console.error('Failed to fetch order risk:', err); return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.action.should-render', async ({data, query}) => { // Need at least one order selected if (!data.selected || data.selected.length === 0) { return {display: false}; } const orderId = data.selected[0].id; try { const {data: responseData, errors} = await query( `query GetOrderRisk($id: ID!) { order(id: $id) { id risk { assessments { riskLevel provider { title } } } } }`, {variables: {id: orderId}} ); if (errors || !responseData?.order?.risk?.assessments) { return {display: false}; } const assessments = responseData.order.risk.assessments; // Check if any assessment indicates high or medium risk const hasElevatedRisk = assessments.some( (assessment: {riskLevel: string}) => assessment.riskLevel === 'HIGH' || assessment.riskLevel === 'MEDIUM' ); return {display: hasElevatedRisk}; } catch (err) { console.error('Failed to fetch order risk:', err); return {display: false}; } } );
Anchor to Order index selection action ,[object Object]Order index selection action target
admin.order-index.selection-action.render
Renders a selection action extension on the order index page when one or more orders are selected. Merchants can access this extension from the bulk actions menu when selecting orders. Use this target to provide batch operations like bulk fulfillment, mass exports, or multi-order processing workflows.
Extensions at this target receive a list of selected order IDs through the data.selected property, enabling bulk processing of multiple orders in a single action.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add a selection action extension that assigns a fulfillment location to multiple selected orders. This example demonstrates processing bulk order selections and updating fulfillment settings.
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-index.selection-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [includeLineItems, setIncludeLineItems] = useState(true); const [includeCustomer, setIncludeCustomer] = useState(true); const [markAsExported, setMarkAsExported] = useState(false); const selectedOrders = data.selected; const orderCount = selectedOrders.length; const handleExport = async () => { setLoading(true); setError(null); try { const orderIds = selectedOrders.map(order => order.id); const response = await fetch('https://your-app.com/api/erp/export-orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds, options: { includeLineItems, includeCustomer, markAsExported, }, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Export failed'); } setSuccess(true); close(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to export orders'); } finally { setLoading(false); } }; return ( <AdminAction title={`Export ${orderCount} Order${orderCount > 1 ? 's' : ''} to ERP`} primaryAction={ <Button onPress={handleExport} disabled={loading || success}> {loading ? 'Exporting...' : `Export ${orderCount} Order${orderCount > 1 ? 's' : ''}`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success"> Successfully exported {orderCount} order{orderCount > 1 ? 's' : ''} to ERP system! </Banner> )} {error && <Banner tone="critical">{error}</Banner>} <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={includeLineItems} onChange={setIncludeLineItems} > Include line item details </Checkbox> <Checkbox checked={includeCustomer} onChange={setIncludeCustomer} > Include customer information </Checkbox> <Divider /> <Checkbox checked={markAsExported} onChange={setMarkAsExported} > Mark orders as exported in Shopify </Checkbox> </BlockStack> </Box> <Banner tone="info"> {orderCount} order{orderCount > 1 ? 's' : ''} selected for export </Banner> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.selection-action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let includeLineItems = true; let includeCustomer = true; let markAsExported = false; const selectedOrders = api.data.selected; const orderCount = selectedOrders.length; const handleExport = async () => { loading = true; error = null; updateUI(); try { const orderIds = selectedOrders.map(order => order.id); const response = await fetch('https://your-app.com/api/erp/export-orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds, options: { includeLineItems, includeCustomer, markAsExported }, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Export failed'); } success = true; updateUI(); api.close(); } catch (err) { error = err instanceof Error ? err.message : 'Failed to export orders'; updateUI(); } finally { loading = false; updateUI(); } }; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handleExport, disabled: loading || success }, loading ? 'Exporting...' : `Export ${orderCount} Order${orderCount > 1 ? 's' : ''}` ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, `Successfully exported ${orderCount} order${orderCount > 1 ? 's' : ''} to ERP system!` ) ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent(Checkbox, { checked: includeLineItems, onChange: (val: boolean) => { includeLineItems = val; updateUI(); }, }, 'Include line item details') ); optionsStack.appendChild( root.createComponent(Checkbox, { checked: includeCustomer, onChange: (val: boolean) => { includeCustomer = val; updateUI(); }, }, 'Include customer information') ); optionsStack.appendChild(root.createComponent(Divider, {})); optionsStack.appendChild( root.createComponent(Checkbox, { checked: markAsExported, onChange: (val: boolean) => { markAsExported = val; updateUI(); }, }, 'Mark orders as exported in Shopify') ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); stack.appendChild( root.createComponent(Banner, { tone: 'info' }, `${orderCount} order${orderCount > 1 ? 's' : ''} selected for export` ) ); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: `Export ${orderCount} Order${orderCount > 1 ? 's' : ''} to ERP`, primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add a selection action extension that exports selected orders to a CSV file. This example demonstrates processing multiple order selections and generating downloadable export files.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-index.selection-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(false); const [fetching, setFetching] = useState(true); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const [orders, setOrders] = useState([]); const [includeBarcode, setIncludeBarcode] = useState(true); const [includePrices, setIncludePrices] = useState(false); useEffect(() => { fetchOrderDetails(); }, []); const fetchOrderDetails = async () => { setFetching(true); try { const orderPromises = data.selected.map(async (selected) => { const result = await query( `query GetOrder($id: ID!) { order(id: $id) { id name createdAt shippingAddress { name address1 city provinceCode zip country } lineItems(first: 50) { edges { node { title quantity sku } } } } }`, { variables: { id: selected.id } } ); return result?.data?.order; }); const fetchedOrders = await Promise.all(orderPromises); setOrders(fetchedOrders.filter(Boolean)); } catch (err) { setError('Failed to fetch order details'); } finally { setFetching(false); } }; const handleGenerate = async () => { setLoading(true); setError(''); try { const response = await fetch('https://your-app.com/api/packing-slips', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders, options: { includeBarcode, includePrices }, }), }); if (response.ok) { setSuccess(true); close(); } else { setError('Failed to generate packing slips'); } } catch (err) { setError('Connection error. Please try again.'); } finally { setLoading(false); } }; return ( <AdminAction title="Generate Packing Slips" primaryAction={ <Button onPress={handleGenerate} disabled={loading || fetching || success}> {loading ? 'Generating...' : `Generate ${orders.length} Slip(s)`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Packing slips generated! Check your email.</Banner>} {error && <Banner tone="critical">{error}</Banner>} {fetching ? ( <Banner tone="info">Loading order details...</Banner> ) : ( <> <Box padding="base"> <BlockStack gap="base"> {orders.map((order) => ( <Box key={order.id} padding="small"> <BlockStack gap="tight"> <Banner tone="info"> {order.name} - {order.shippingAddress?.city || 'No address'} {` (${order.lineItems.edges.length} items)`} </Banner> </BlockStack> </Box> ))} </BlockStack> </Box> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode}> Include order barcode </Checkbox> <Checkbox checked={includePrices} onChange={setIncludePrices}> Include item prices </Checkbox> </BlockStack> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.selection-action.render', (root, api) => { let loading = false; let fetching = true; let success = false; let error = ''; let orders: any[] = []; let includeBarcode = true; let includePrices = false; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const primaryButton = root.createComponent( Button, { onPress: handleGenerate, disabled: true }, 'Loading...' ); primaryAction.appendChild(primaryButton); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); async function fetchOrderDetails() { fetching = true; updateUI(); try { const orderPromises = api.data.selected.map(async (selected) => { const result = await api.query( `query GetOrder($id: ID!) { order(id: $id) { id name createdAt shippingAddress { name address1 city provinceCode zip } lineItems(first: 50) { edges { node { title quantity sku } } } } }`, { variables: { id: selected.id } } ); return result?.data?.order; }); orders = (await Promise.all(orderPromises)).filter(Boolean); } catch (err) { error = 'Failed to fetch order details'; } finally { fetching = false; updateUI(); } } async function handleGenerate() { loading = true; error = ''; updateUI(); try { const response = await fetch('https://your-app.com/api/packing-slips', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders, options: { includeBarcode, includePrices }, }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { error = 'Failed to generate packing slips'; } } catch (err) { error = 'Connection error. Please try again.'; } finally { loading = false; updateUI(); } } function updateUI() { content.replaceChildren(); primaryButton.updateProps({ disabled: loading || fetching || success, }); primaryButton.replaceChildren( loading ? 'Generating...' : `Generate ${orders.length} Slip(s)` ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Packing slips generated!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (fetching) { stack.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading order details...') ); } else { const ordersBox = root.createComponent(Box, { padding: 'base' }); const ordersStack = root.createComponent(BlockStack, { gap: 'base' }); orders.forEach((order) => { ordersStack.appendChild( root.createComponent( Banner, { tone: 'info' }, `${order.name} - ${order.shippingAddress?.city || 'No address'} (${order.lineItems.edges.length} items)` ) ); }); ordersBox.appendChild(ordersStack); stack.appendChild(ordersBox); stack.appendChild(root.createComponent(Divider, {})); const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent( Checkbox, { checked: includeBarcode, onChange: (val: boolean) => { includeBarcode = val; updateUI(); }, }, 'Include order barcode' ) ); optionsStack.appendChild( root.createComponent( Checkbox, { checked: includePrices, onChange: (val: boolean) => { includePrices = val; updateUI(); }, }, 'Include item prices' ) ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); } content.appendChild(stack); } const adminAction = root.createComponent(AdminAction, { title: 'Generate Packing Slips', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); fetchOrderDetails(); root.mount(); } );
Anchor to Order index selection action (should render) ,[object Object]Order index selection action (should render) target
admin.order-index.selection-action.should-render
Controls the render state of a selection action extension on the order index page. Use this target to conditionally show or hide your action extension based on the order's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add a should-render extension that checks with your app backend whether selected orders have fulfillment tracking available. This example demonstrates calling a custom API endpoint to determine if orders are eligible for real-time fulfillment progress display.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.selection-action.should-render', async ({data}) => { const selectedOrderIds = data.selected.map((item) => item.id); if (selectedOrderIds.length === 0) { return {display: false}; } try { const response = await fetch('https://your-app.com/api/fulfillment/check-tracking', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ orderIds: selectedOrderIds, }), }); if (!response.ok) { return {display: false}; } const result = await response.json(); // Show action if any selected order has tracking available const hasTrackingAvailable = result.orders?.some( (order: {hasTracking: boolean}) => order.hasTracking ); return {display: hasTrackingAvailable}; } catch (err) { console.error('Failed to check fulfillment tracking:', err); return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.selection-action.should-render', async ({data}) => { const selectedOrderIds = data.selected.map((item) => item.id); if (selectedOrderIds.length === 0) { return {display: false}; } try { const response = await fetch('https://your-app.com/api/fulfillment/check-tracking', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ orderIds: selectedOrderIds, }), }); if (!response.ok) { return {display: false}; } const result = await response.json(); // Show action if any selected order has tracking available const hasTrackingAvailable = result.orders?.some( (order: {hasTracking: boolean}) => order.hasTracking ); return {display: hasTrackingAvailable}; } catch (err) { console.error('Failed to check fulfillment tracking:', err); return {display: false}; } } );Description
Add a should-render extension that displays a fraud review action only for orders with high or medium risk levels. This example demonstrates querying the GraphQL Admin API to check order risk assessments before showing the action.
React
import {extension} from '@shopify/ui-extensions/admin'; const TARGET = 'admin.order-index.selection-action.should-render'; export default extension(TARGET, async ({data, query}) => { const selectedOrderIds = data.selected.map((item) => item.id); // Check if any selected orders have elevated risk try { const results = await Promise.all( selectedOrderIds.map(async (orderId) => { const {data: orderData, errors} = await query( `query GetOrderRisk($id: ID!) { order(id: $id) { id risk { level assessments { riskLevel provider { ... on App { title } } facts { description sentiment } } } } }`, {variables: {id: orderId}} ); if (errors) { console.error('Error fetching order risk:', errors); return false; } const riskLevel = orderData?.order?.risk?.level; return riskLevel === 'HIGH' || riskLevel === 'MEDIUM'; }) ); // Show action if at least one order has elevated risk const hasRiskyOrders = results.some((isRisky) => isRisky); return {display: hasRiskyOrders}; } catch (err) { console.error('Failed to check order risk:', err); return {display: false}; } });TS
import {extension} from '@shopify/ui-extensions/admin'; const TARGET = 'admin.order-index.selection-action.should-render'; export default extension(TARGET, async ({data, query}) => { const selectedOrderIds = data.selected.map((item) => item.id); // Check if any selected orders have elevated risk try { const results = await Promise.all( selectedOrderIds.map(async (orderId) => { const {data: orderData, errors} = await query( `query GetOrderRisk($id: ID!) { order(id: $id) { id risk { level assessments { riskLevel provider { ... on App { title } } facts { description sentiment } } } } }`, {variables: {id: orderId}} ); if (errors) { console.error('Error fetching order risk:', errors); return false; } const riskLevel = orderData?.order?.risk?.level; return riskLevel === 'HIGH' || riskLevel === 'MEDIUM'; }) ); // Show action if at least one order has elevated risk const hasRiskyOrders = results.some((isRisky) => isRisky); return {display: hasRiskyOrders}; } catch (err) { console.error('Failed to check order risk:', err); return {display: false}; } });
Anchor to Order index selection print action ,[object Object]Order index selection print action target
admin.order-index.selection-print-action.render
Renders an admin print action extension on the order index page when multiple orders are selected. Merchants access this extension from the Print menu in the bulk actions toolbar. Use this target to provide bulk print workflows such as generating packing slips, shipping labels, or invoices for multiple orders at once.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add a selection print action extension that generates packing slips for multiple orders at once. This example demonstrates batch processing selected orders and preparing bulk print output.
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-index.selection-print-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [includeLineItems, setIncludeLineItems] = useState(true); const [includeCustomer, setIncludeCustomer] = useState(true); const [exportFormat, setExportFormat] = useState(['full']); const selectedOrders = data.selected; const handleExport = async () => { setLoading(true); setError(null); try { const orderIds = selectedOrders.map(order => order.id); const response = await fetch('https://your-app.com/api/erp/export-orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds, options: { includeLineItems, includeCustomer, format: exportFormat[0], }, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Export failed'); } setSuccess(true); close(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to export orders'); } finally { setLoading(false); } }; return ( <AdminAction title="Export to ERP" primaryAction={ <Button onPress={handleExport} disabled={loading || success}> {loading ? 'Exporting...' : `Export ${selectedOrders.length} Order(s)`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success"> Successfully exported {selectedOrders.length} order(s) to ERP system! </Banner> )} {error && <Banner tone="critical">{error}</Banner>} <Box padding="base"> <BlockStack gap="base"> <ChoiceList name="exportFormat" title="Export Format" value={exportFormat} onChange={setExportFormat} choices={[ { id: 'full', label: 'Full order data' }, { id: 'summary', label: 'Summary only' }, { id: 'financial', label: 'Financial data only' }, ]} /> <Divider /> <Checkbox checked={includeLineItems} onChange={setIncludeLineItems} > Include line item details </Checkbox> <Checkbox checked={includeCustomer} onChange={setIncludeCustomer} > Include customer information </Checkbox> </BlockStack> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.selection-print-action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let includeLineItems = true; let includeCustomer = true; let exportFormat = ['full']; const selectedOrders = api.data.selected; const handleExport = async () => { loading = true; error = null; updateUI(); try { const orderIds = selectedOrders.map(order => order.id); const response = await fetch('https://your-app.com/api/erp/export-orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds, options: { includeLineItems, includeCustomer, format: exportFormat[0] }, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Export failed'); } success = true; updateUI(); api.close(); } catch (err) { error = err instanceof Error ? err.message : 'Failed to export orders'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const primaryButton = root.createComponent( Button, { onPress: handleExport, disabled: loading || success }, `Export ${selectedOrders.length} Order(s)` ); primaryAction.appendChild(primaryButton); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); const updateUI = () => { content.replaceChildren(); primaryButton.updateProps({ disabled: loading || success }); primaryButton.replaceChildren(loading ? 'Exporting...' : `Export ${selectedOrders.length} Order(s)`); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, `Successfully exported ${selectedOrders.length} order(s) to ERP system!` ) ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'exportFormat', title: 'Export Format', value: exportFormat, onChange: (val: string[]) => { exportFormat = val; updateUI(); }, choices: [ { id: 'full', label: 'Full order data' }, { id: 'summary', label: 'Summary only' }, { id: 'financial', label: 'Financial data only' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent(Checkbox, { checked: includeLineItems, onChange: (val: boolean) => { includeLineItems = val; }, }, 'Include line item details') ); innerStack.appendChild( root.createComponent(Checkbox, { checked: includeCustomer, onChange: (val: boolean) => { includeCustomer = val; }, }, 'Include customer information') ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Export to ERP', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add a selection print action extension that generates invoices for multiple orders. This example demonstrates using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) to fetch order details and prepare bulk invoice output.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-index.selection-print-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [orders, setOrders] = useState([]); const [includeBarcode, setIncludeBarcode] = useState(true); const [includeGiftMessage, setIncludeGiftMessage] = useState(false); const [error, setError] = useState(null); useEffect(() => { fetchOrderDetails(); }, []); const fetchOrderDetails = async () => { try { const orderIds = data.selected.map(s => s.id); const results = await Promise.all( orderIds.map(id => query(`query GetOrder($id: ID!) { order(id: $id) { id name createdAt shippingAddress { name address1 city provinceCode zip country } lineItems(first: 50) { edges { node { title quantity sku } } } } }`, { variables: { id } }) ) ); const fetchedOrders = results .filter(r => r.data?.order) .map(r => r.data.order); setOrders(fetchedOrders); } catch (err) { setError('Failed to fetch order details'); } finally { setLoading(false); } }; const handlePrint = () => { const printData = { orders, options: { includeBarcode, includeGiftMessage } }; console.log('Generating packing slips:', printData); window.print(); close(); }; if (loading) { return ( <AdminAction title="Generate Packing Slips"> <Banner tone="info">Loading order details...</Banner> </AdminAction> ); } return ( <AdminAction title="Generate Packing Slips" primaryAction={ <Button onPress={handlePrint} disabled={orders.length === 0}> Print {orders.length} Slip{orders.length !== 1 ? 's' : ''} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {error && <Banner tone="critical">{error}</Banner>} <Banner tone="info"> {orders.length} order{orders.length !== 1 ? 's' : ''} selected for packing slip generation </Banner> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode} > Include order barcode </Checkbox> <Checkbox checked={includeGiftMessage} onChange={setIncludeGiftMessage} > Include gift message (if available) </Checkbox> </BlockStack> </Box> <Divider /> <Box padding="base"> <BlockStack gap="tight"> {orders.map(order => ( <Banner key={order.id} tone="success"> {order.name} - {order.lineItems.edges.length} item(s) → {order.shippingAddress?.city || 'No address'} </Banner> ))} </BlockStack> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.selection-print-action.render', async (root, api) => { let orders: any[] = []; let includeBarcode = true; let includeGiftMessage = false; let loading = true; let error: string | null = null; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const printButton = root.createComponent( Button, { onPress: handlePrint, disabled: true }, 'Print 0 Slips' ); primaryAction.appendChild(printButton); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); function handlePrint() { const printData = { orders, options: { includeBarcode, includeGiftMessage } }; console.log('Generating packing slips:', printData); window.print(); api.close(); } function updateUI() { content.replaceChildren(); if (loading) { content.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading order details...') ); return; } const stack = root.createComponent(BlockStack, { gap: 'base' }); if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } stack.appendChild( root.createComponent( Banner, { tone: 'info' }, `${orders.length} order${orders.length !== 1 ? 's' : ''} selected for packing slip generation` ) ); stack.appendChild(root.createComponent(Divider, {})); const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent( Checkbox, { checked: includeBarcode, onChange: (val) => { includeBarcode = val; } }, 'Include order barcode' ) ); optionsStack.appendChild( root.createComponent( Checkbox, { checked: includeGiftMessage, onChange: (val) => { includeGiftMessage = val; } }, 'Include gift message (if available)' ) ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); stack.appendChild(root.createComponent(Divider, {})); const ordersBox = root.createComponent(Box, { padding: 'base' }); const ordersStack = root.createComponent(BlockStack, { gap: 'tight' }); orders.forEach(order => { ordersStack.appendChild( root.createComponent( Banner, { tone: 'success' }, `${order.name} - ${order.lineItems.edges.length} item(s) → ${order.shippingAddress?.city || 'No address'}` ) ); }); ordersBox.appendChild(ordersStack); stack.appendChild(ordersBox); content.appendChild(stack); printButton.updateProps({ disabled: orders.length === 0, }); printButton.replaceChildren(`Print ${orders.length} Slip${orders.length !== 1 ? 's' : ''}`); } async function fetchOrderDetails() { try { const orderIds = api.data.selected.map(s => s.id); const results = await Promise.all( orderIds.map(id => api.query(`query GetOrder($id: ID!) { order(id: $id) { id name createdAt shippingAddress { name address1 city provinceCode zip country } lineItems(first: 50) { edges { node { title quantity sku } } } } }`, { variables: { id } }) ) ); orders = results.filter(r => r.data?.order).map(r => r.data.order); } catch (err) { error = 'Failed to fetch order details'; } finally { loading = false; updateUI(); } } updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Generate Packing Slips', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); await fetchOrderDetails(); } );
Anchor to Order index selection print action (should render) ,[object Object]Order index selection print action (should render) target
admin.order-index.selection-print-action.should-render
Controls the render state of an admin action extension on the order index page. Use this target to conditionally show or hide your action extension based on the order's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add an action extension that validates selected orders against your ERP system before enabling the print action. This example shows how to check order eligibility with your backend API and control whether the print action should render based on business rules.
React
import React from 'react'; import { useState, useEffect } from 'react'; import { reactExtension, useApi, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-index.selection-print-action.should-render'; export default reactExtension(TARGET, () => <App />); function App() { const { data } = useApi(TARGET); useEffect(() => { async function checkPrintEligibility() { const orderIds = data.selected.map((item) => item.id); try { // Check with your backend if these orders can be printed for ERP const response = await fetch('https://your-app.com/api/erp/check-print-eligibility', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds }), }); if (response.ok) { const result = await response.json(); // Return true if all orders are eligible for ERP print export // The should-render target uses the response to show/hide the action return result.allEligible; } return false; } catch (err) { // On error, don't show the print action return false; } } checkPrintEligibility(); }, [data.selected]); // should-render targets don't render UI, they just determine visibility return null; }TS
import { extension, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.selection-print-action.should-render', async (root, api) => { const orderIds = api.data.selected.map((item) => item.id); try { // Check with your backend if these orders can be printed for ERP const response = await fetch('https://your-app.com/api/erp/check-print-eligibility', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderIds }), }); if (response.ok) { const result = await response.json(); // Business logic: only show print action if orders meet criteria // - All orders must be fulfilled // - All orders must not already be exported to ERP // - Customer must have valid ERP account linked if (result.allEligible && result.erpAccountsValid) { // Returning without mounting indicates the action should render root.mount(); return; } } } catch (err) { // On error, don't show the print action console.error('ERP eligibility check failed:', err); } // Mount is still called but the should-render logic determines visibility root.mount(); } );Description
Add an action extension that generates custom packing slips for selected orders by fetching order details using the GraphQL Admin API. This example demonstrates using query() to retrieve order line items, shipping addresses, and customer information for print-ready output.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-index.selection-print-action.should-render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(false); const [orders, setOrders] = useState([]); const [includeBarcode, setIncludeBarcode] = useState(true); const [includePrice, setIncludePrice] = useState(false); const [error, setError] = useState(null); useEffect(() => { fetchOrderDetails(); }, []); const fetchOrderDetails = async () => { try { const orderIds = data.selected.map(s => s.id); const results = await Promise.all( orderIds.map(id => query( `query GetOrder($id: ID!) { order(id: $id) { id name createdAt shippingAddress { name address1 city provinceCode zip country } lineItems(first: 20) { edges { node { title quantity sku } } } } }`, { variables: { id } } )) ); const fetchedOrders = results .filter(r => r.data?.order) .map(r => r.data.order); setOrders(fetchedOrders); } catch (err) { setError('Failed to fetch order details'); } finally { setLoading(false); } }; const handleGenerate = async () => { setGenerating(true); try { // Simulate packing slip generation await new Promise(resolve => setTimeout(resolve, 1500)); window.print(); close(); } catch (err) { setError('Failed to generate packing slip'); setGenerating(false); } }; const totalItems = orders.reduce((sum, order) => sum + order.lineItems.edges.reduce((s, e) => s + e.node.quantity, 0), 0 ); return ( <AdminAction title="Generate Packing Slip" primaryAction={ <Button onPress={handleGenerate} disabled={loading || generating || orders.length === 0}> {generating ? 'Generating...' : `Print ${orders.length} Slip(s)`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {error && <Banner tone="critical">{error}</Banner>} {loading ? ( <Banner tone="info">Loading order details...</Banner> ) : ( <> <Banner tone="info"> {orders.length} order(s) selected with {totalItems} total items </Banner> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode} > Include barcode for scanning </Checkbox> <Checkbox checked={includePrice} onChange={setIncludePrice} > Include item prices </Checkbox> </BlockStack> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-index.selection-print-action.should-render', async (root, api) => { let loading = true; let generating = false; let orders = []; let includeBarcode = true; let includePrice = false; let error = null; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const totalItems = orders.reduce((sum, order) => sum + order.lineItems.edges.reduce((s, e) => s + e.node.quantity, 0), 0 ); primaryAction.appendChild( root.createComponent( Button, { onPress: handleGenerate, disabled: loading || generating || orders.length === 0 }, generating ? 'Generating...' : `Print ${orders.length} Slip(s)` ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (loading) { stack.appendChild(root.createComponent(Banner, { tone: 'info' }, 'Loading order details...')); } else { stack.appendChild( root.createComponent(Banner, { tone: 'info' }, `${orders.length} order(s) selected with ${totalItems} total items` ) ); stack.appendChild(root.createComponent(Divider)); const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent( Checkbox, { checked: includeBarcode, onChange: (val) => { includeBarcode = val; updateUI(); } }, 'Include barcode for scanning' ) ); optionsStack.appendChild( root.createComponent( Checkbox, { checked: includePrice, onChange: (val) => { includePrice = val; updateUI(); } }, 'Include item prices' ) ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); } content.appendChild(stack); }; const handleGenerate = async () => { generating = true; updateUI(); try { await new Promise(resolve => setTimeout(resolve, 1500)); api.close(); } catch (err) { error = 'Failed to generate packing slip'; generating = false; updateUI(); } }; const fetchOrderDetails = async () => { try { const orderIds = api.data.selected.map(s => s.id); const results = await Promise.all( orderIds.map(id => api.query( `query GetOrder($id: ID!) { order(id: $id) { id name shippingAddress { name address1 city provinceCode zip country } lineItems(first: 20) { edges { node { title quantity sku } } } } }`, { variables: { id } } )) ); orders = results.filter(r => r.data?.order).map(r => r.data.order); } catch (err) { error = 'Failed to fetch order details'; } finally { loading = false; updateUI(); } }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Generate Packing Slip', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); await fetchOrderDetails(); } );
Anchor to Order fulfilled card targetsOrder fulfilled card targets
Use action targets to extend the order fulfilled card page with workflows and operations.
Anchor to Order fulfilled card action ,[object Object]Order fulfilled card action target
admin.order-fulfilled-card.action.render
Renders an admin action extension on the order details page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on order data, such as syncing with external systems, exporting order information, or managing credit terms.
Extensions at this target can access order data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that pushes fulfilled order data to an external ERP system. This example demonstrates calling your app backend with order details, handling export options, and providing user feedback on sync status.
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-fulfilled-card.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [includeTracking, setIncludeTracking] = useState(true); const [includeCustomer, setIncludeCustomer] = useState(true); const [exportFormat, setExportFormat] = useState(['standard']); const handleExport = async () => { setLoading(true); setError(null); const orderId = data.selected[0].id; try { const response = await fetch('https://your-app.com/api/erp/export-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, includeTracking, includeCustomer, format: exportFormat[0], }), }); if (response.ok) { setSuccess(true); close(); } else { const result = await response.json(); setError(result.message || 'Export failed'); } } catch (err) { setError('Connection to ERP failed. Please try again.'); } finally { setLoading(false); } }; return ( <AdminAction title="Export to ERP System" primaryAction={ <Button onPress={handleExport} disabled={loading || success}> {loading ? 'Exporting...' : 'Export Order'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success">Order successfully exported to ERP!</Banner> )} {error && <Banner tone="critical">{error}</Banner>} <Box padding="base"> <BlockStack gap="base"> <ChoiceList name="exportFormat" title="Export Format" value={exportFormat} onChange={setExportFormat} choices={[ { label: 'Standard (JSON)', value: 'standard' }, { label: 'Extended (with line items)', value: 'extended' }, { label: 'Minimal (IDs only)', value: 'minimal' }, ]} /> <Divider /> <Checkbox checked={includeTracking} onChange={setIncludeTracking} > Include fulfillment tracking info </Checkbox> <Checkbox checked={includeCustomer} onChange={setIncludeCustomer} > Include customer details </Checkbox> </BlockStack> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-fulfilled-card.action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let includeTracking = true; let includeCustomer = true; let exportFormat = ['standard']; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const handleExport = async () => { loading = true; error = null; updateUI(); const orderId = api.data.selected[0].id; try { const response = await fetch('https://your-app.com/api/erp/export-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, includeTracking, includeCustomer, format: exportFormat[0], }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { const result = await response.json(); error = result.message || 'Export failed'; } } catch (err) { error = 'Connection to ERP failed. Please try again.'; } finally { loading = false; updateUI(); } }; const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handleExport, disabled: loading || success }, loading ? 'Exporting...' : 'Export Order' ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Order successfully exported to ERP!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'exportFormat', title: 'Export Format', value: exportFormat, onChange: (val: string[]) => { exportFormat = val; updateUI(); }, choices: [ { label: 'Standard (JSON)', value: 'standard' }, { label: 'Extended (with line items)', value: 'extended' }, { label: 'Minimal (IDs only)', value: 'minimal' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent( Checkbox, { checked: includeTracking, onChange: (val: boolean) => { includeTracking = val; } }, 'Include fulfillment tracking info' ) ); innerStack.appendChild( root.createComponent( Checkbox, { checked: includeCustomer, onChange: (val: boolean) => { includeCustomer = val; } }, 'Include customer details' ) ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Export to ERP System', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that creates a delivery confirmation document for fulfilled orders. This example demonstrates using the GraphQL Admin API to fetch fulfillment data and generate a customer-facing confirmation.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.order-fulfilled-card.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(false); const [generating, setGenerating] = useState(false); const [orderData, setOrderData] = useState(null); const [error, setError] = useState(''); const [options, setOptions] = useState({ includeBarcode: true, includePrices: false, includeNotes: true, }); useEffect(() => { fetchOrderDetails(); }, []); const fetchOrderDetails = async () => { setLoading(true); const orderId = data.selected[0].id; try { const result = await query(` query GetOrderForPackingSlip($id: ID!) { order(id: $id) { id name createdAt note shippingAddress { name address1 address2 city province zip country } lineItems(first: 50) { edges { node { title quantity sku variant { title } } } } fulfillments(first: 1) { trackingInfo { number company } } } } `, { variables: { id: orderId } }); if (result.data?.order) { setOrderData(result.data.order); } else { setError('Could not load order details'); } } catch (err) { setError('Failed to fetch order data'); } finally { setLoading(false); } }; const handleGenerate = async () => { setGenerating(true); // Simulate PDF generation await new Promise(resolve => setTimeout(resolve, 1500)); setGenerating(false); close(); }; const itemCount = orderData?.lineItems?.edges?.length || 0; return ( <AdminAction title="Generate Packing Slip" primaryAction={ <Button onPress={handleGenerate} disabled={loading || generating || !orderData}> {generating ? 'Generating...' : 'Generate PDF'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {error && <Banner tone="critical">{error}</Banner>} {loading && <Banner tone="info">Loading order details...</Banner>} {orderData && ( <> <Banner tone="success"> Order {orderData.name} - {itemCount} items ready for packing slip </Banner> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={options.includeBarcode} onChange={(checked) => setOptions({ ...options, includeBarcode: checked })} > Include barcode for scanning </Checkbox> <Checkbox checked={options.includePrices} onChange={(checked) => setOptions({ ...options, includePrices: checked })} > Include item prices </Checkbox> <Checkbox checked={options.includeNotes} onChange={(checked) => setOptions({ ...options, includeNotes: checked })} > Include order notes </Checkbox> </BlockStack> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-fulfilled-card.action.render', async (root, api) => { let loading = true; let generating = false; let orderData: any = null; let error = ''; let options = { includeBarcode: true, includePrices: false, includeNotes: true, }; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const itemCount = orderData?.lineItems?.edges?.length || 0; primaryAction.appendChild( root.createComponent( Button, { onPress: handleGenerate, disabled: loading || generating || !orderData, }, generating ? 'Generating...' : 'Generate PDF' ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (loading) { stack.appendChild(root.createComponent(Banner, { tone: 'info' }, 'Loading order details...')); } if (orderData) { stack.appendChild( root.createComponent( Banner, { tone: 'success' }, `Order ${orderData.name} - ${itemCount} items ready for packing slip` ) ); stack.appendChild(root.createComponent(Divider, {})); const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent( Checkbox, { checked: options.includeBarcode, onChange: (checked: boolean) => { options.includeBarcode = checked; updateUI(); }, }, 'Include barcode for scanning' ) ); optionsStack.appendChild( root.createComponent( Checkbox, { checked: options.includePrices, onChange: (checked: boolean) => { options.includePrices = checked; updateUI(); }, }, 'Include item prices' ) ); optionsStack.appendChild( root.createComponent( Checkbox, { checked: options.includeNotes, onChange: (checked: boolean) => { options.includeNotes = checked; updateUI(); }, }, 'Include order notes' ) ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); } content.appendChild(stack); }; const handleGenerate = async () => { generating = true; updateUI(); await new Promise(resolve => setTimeout(resolve, 1500)); generating = false; api.close(); }; const fetchOrderDetails = async () => { const orderId = api.data.selected[0].id; try { const result = await api.query(` query GetOrderForPackingSlip($id: ID!) { order(id: $id) { id name createdAt note shippingAddress { name address1 address2 city province zip country } lineItems(first: 50) { edges { node { title quantity sku variant { title } } } } fulfillments(first: 1) { trackingInfo { number company } } } } `, { variables: { id: orderId } }); if (result.data?.order) { orderData = result.data.order; } else { error = 'Could not load order details'; } } catch (err) { error = 'Failed to fetch order data'; } finally { loading = false; updateUI(); } }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Generate Packing Slip', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); await fetchOrderDetails(); } );
Anchor to Order fulfilled card action (should render) ,[object Object]Order fulfilled card action (should render) target
admin.order-fulfilled-card.action.should-render
Controls the render state of an admin action extension on the order details page. Use this target to conditionally show or hide your action extension based on the order's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add a should-render extension that checks with your app backend whether the fulfilled order has tracking updates available or requires carrier sync actions.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-fulfilled-card.action.should-render', async ({data}) => { const orderId = data.selected[0].id; const orderGid = orderId.split('/').pop(); try { const response = await fetch( `https://your-app.com/api/fulfillment/tracking-status?orderId=${orderGid}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, } ); if (!response.ok) { return {display: false}; } const trackingData = await response.json(); // Show action if tracking updates are available or sync is needed const shouldDisplay = trackingData.hasTrackingUpdates || trackingData.requiresCarrierSync || trackingData.deliveryExceptionDetected; return {display: shouldDisplay}; } catch (err) { console.error('Failed to check fulfillment tracking status:', err); return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.order-fulfilled-card.action.should-render', async ({data}) => { const orderId = data.selected[0].id; const orderGid = orderId.split('/').pop(); try { const response = await fetch( `https://your-app.com/api/fulfillment/tracking-status?orderId=${orderGid}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, } ); if (!response.ok) { return {display: false}; } const trackingData = await response.json(); // Show action if tracking updates are available or sync is needed const shouldDisplay = trackingData.hasTrackingUpdates || trackingData.requiresCarrierSync || trackingData.deliveryExceptionDetected; return {display: shouldDisplay}; } catch (err) { console.error('Failed to check fulfillment tracking status:', err); return {display: false}; } } );Description
Add a should-render extension that conditionally displays a fraud risk assessment action only for orders that have been flagged with medium or high risk levels by Shopify's fraud analysis.
React
import {extension} from '@shopify/ui-extensions/admin'; const TARGET = 'admin.order-fulfilled-card.action.should-render'; export default extension(TARGET, async ({data, query}) => { const orderId = data.selected[0].id; try { const {data: orderData, errors} = await query( `query GetOrderRisk($id: ID!) { order(id: $id) { id risk { assessments { riskLevel provider { title } } } } }`, {variables: {id: orderId}} ); if (errors || !orderData?.order) { return {display: false}; } const assessments = orderData.order.risk?.assessments || []; // Show action if any assessment indicates medium or high risk const hasElevatedRisk = assessments.some( (assessment: {riskLevel: string}) => assessment.riskLevel === 'MEDIUM' || assessment.riskLevel === 'HIGH' ); return {display: hasElevatedRisk}; } catch (err) { console.error('Failed to fetch order risk data:', err); return {display: false}; } });TS
import {extension} from '@shopify/ui-extensions/admin'; const TARGET = 'admin.order-fulfilled-card.action.should-render'; export default extension(TARGET, async ({data, query}) => { const orderId = data.selected[0].id; try { const {data: orderData, errors} = await query( `query GetOrderRisk($id: ID!) { order(id: $id) { id risk { assessments { riskLevel provider { title } } } } }`, {variables: {id: orderId}} ); if (errors || !orderData?.order) { return {display: false}; } const assessments = orderData.order.risk?.assessments || []; // Show action if any assessment indicates medium or high risk const hasElevatedRisk = assessments.some( (assessment: {riskLevel: string}) => assessment.riskLevel === 'MEDIUM' || assessment.riskLevel === 'HIGH' ); return {display: hasElevatedRisk}; } catch (err) { console.error('Failed to fetch order risk data:', err); return {display: false}; } });
Anchor to Best practicesBest practices
- Check fulfillment status before action: Before displaying fulfillment-related actions, verify the order's fulfillment status using
displayFulfillmentStatus. This prevents showing "ship order" actions on already-fulfilled orders or offering refunds on cancelled orders. - Batch bulk operations intelligently: When processing multiple orders, work in batches to stay within GraphQL query cost limits. Display progress for operations on more than 10 orders so merchants can track completion of long-running tasks.
- Combine risk indicators: When displaying fraud risk assessments, show both Shopify's native risk level and your app's custom scoring. Provide specific reasons for elevated risk (for example, mismatched billing/shipping, high-value first-time customer, unusual velocity) rather than just a score.
- Handle financial fields carefully: Order financial data includes complex fields like refunds, discounts, and tax lines. When calculating totals or displaying financial information, use
MoneyV2fields (amount and currencyCode) and respect the order's presentment currency for accuracy. - Consider order editability: Orders have different editability states based on their status and payment state. Before showing data modification actions, check if the order can be edited using order edit workflows rather than direct mutations.
Anchor to LimitationsLimitations
- Single target per module: Each
[[extensions.targeting]]entry in your TOML configuration maps one target to one module file. - Fulfilled card visibility: The
admin.order-fulfilled-card.action.rendertarget only appears on orders fulfilled by your app's fulfillment service. If your app doesn't act as a fulfillment service or an order was fulfilled by Shopify or another fulfillment provider, this target won't appear. - Print menu location: Print actions appear in the Print menu, not the More actions menu.
- Block target visibility: Block extensions must be manually added and pinned by merchants before they appear.
- Block collapse behavior: Returning
nullfrom a block extension collapses the block rather than removing it from the page. Blocks can't be fully hidden at runtime.