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.
Draft orders
Draft orders allow merchants to create orders on behalf of customers for scenarios like phone orders, wholesale quotes, custom orders, and manual invoicing. Draft order pages display information about individual draft orders and draft order lists. Extensions on these pages help merchants enhance these workflows with custom functionality.
Anchor to Use casesUse cases
- External order management: Sync draft orders with external order management systems, ERPs, or wholesale platforms to maintain consistent order data across systems and support B2B workflows.
- Custom pricing and quotes: Display custom pricing, apply special discounts, calculate complex wholesale pricing, or generate professional quotes with pricing from external systems before converting draft orders.
- Order validation and verification: Validate draft order data against external systems, verify customer credit limits, check inventory availability across warehouses, or flag potential issues before order completion.
- Payment processing workflows: Integrate custom payment workflows, generate payment links for draft orders, process deposits or partial payments, or send payment requests to customers through external payment gateways.
- Bulk draft order operations: Process multiple draft orders at once for operations like bulk conversion, batch invoice generation, mass updates, or exporting draft order data to accounting systems.

Anchor to Draft order details targetsDraft order details targets
Use action and block targets to extend the draft order details page. Add workflows and contextual information that help merchants manage individual draft orders and improve order creation processes.
Action targets open as modal overlays from the More actions menu, while block targets display as inline cards. The examples demonstrate fetching data from Shopify's direct API or your app's backend.
Anchor to Draft order details action ,[object Object]Draft order details action target
admin.draft-order-details.action.render
Renders an admin action extension on the draft order details page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on individual draft orders, such as syncing with external systems, generating quotes, processing payments, or applying custom pricing.
Extensions at this target can access information about the draft order 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 generates a payment link for a draft order. This example shows how to create a workflow that generates a payment URL and sends it to the customer.
React
import React from 'react'; import {useState, useEffect} from 'react'; import { reactExtension, useApi, AdminAction, Banner, Section, BlockStack, Box, Checkbox, TextField, Text, Button, ProgressIndicator, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.draft-order-details.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 [draftOrder, setDraftOrder] = useState(null); const [sendEmail, setSendEmail] = useState(true); const [success, setSuccess] = useState(false); const [error, setError] = useState(false); const [paymentLink, setPaymentLink] = useState(''); useEffect(() => { const fetchDraftOrder = async () => { const draftOrderId = data.selected[0].id; try { // Fetch draft order details from GraphQL Admin API const {data: draftOrderData} = await query( ` query GetDraftOrder($id: ID!) { draftOrder(id: $id) { id name customer { email firstName lastName } totalPriceSet { presentmentMoney { amount currencyCode } } } } `, {variables: {id: draftOrderId}} ); setDraftOrder(draftOrderData.draftOrder); } catch (err) { console.error('Error fetching draft order:', err); } finally { setFetching(false); } }; fetchDraftOrder(); }, [data, query]); const handleGenerate = async () => { setLoading(true); setSuccess(false); setError(false); const draftOrderId = data.selected[0].id; try { // Generate payment link through your app's backend const response = await fetch('https://your-app.com/api/generate-payment-link', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ draftOrderId, sendEmail, }), }); if (response.ok) { const {paymentUrl} = await response.json(); setPaymentLink(paymentUrl); setSuccess(true); } else { setError(true); } } catch (err) { setError(true); } finally { setLoading(false); } }; if (fetching) { return ( <AdminAction title="Generate Payment Link"> <BlockStack> <ProgressIndicator size="small-100" /> <Text>Loading draft order details...</Text> </BlockStack> </AdminAction> ); } return ( <AdminAction title="Generate Payment Link" primaryAction={ <Button onPress={handleGenerate} disabled={loading || success} > {loading ? 'Generating...' : 'Generate Link'} </Button> } secondaryAction={ <Button onPress={close}> {success ? 'Close' : 'Cancel'} </Button> } > {success && ( <Banner tone="success" dismissible> Payment link generated successfully! {sendEmail && ' Email sent to customer.'} </Banner> )} {error && ( <Banner tone="critical" dismissible> Failed to generate payment link. Please try again. </Banner> )} <Section heading="Draft order information"> <BlockStack> <Box> <Text fontWeight="bold">Draft order: </Text> <Text>{draftOrder.name}</Text> </Box> {draftOrder.customer && ( <Box> <Text fontWeight="bold">Customer: </Text> <Text>{draftOrder.customer.firstName} {draftOrder.customer.lastName}</Text> <Text tone="subdued"> ({draftOrder.customer.email})</Text> </Box> )} <Box> <Text fontWeight="bold">Total: </Text> <Text>{draftOrder.totalPriceSet.presentmentMoney.currencyCode} {draftOrder.totalPriceSet.presentmentMoney.amount}</Text> </Box> </BlockStack> </Section> <Section heading="Payment link options"> <BlockStack> <Checkbox checked={sendEmail} onChange={setSendEmail} disabled={!draftOrder.customer?.email} > Send payment link to customer via email </Checkbox> {!draftOrder.customer?.email && ( <Text tone="subdued">No customer email available</Text> )} </BlockStack> </Section> {paymentLink && ( <Section heading="Payment link"> <BlockStack> <TextField label="Payment URL" value={paymentLink} readOnly /> <Button onPress={() => navigator.clipboard.writeText(paymentLink)} variant="secondary" > Copy to clipboard </Button> </BlockStack> </Section> )} </AdminAction> ); }TS
import { extension, AdminAction, Banner, Section, BlockStack, Box, Checkbox, TextField, Text, Button, ProgressIndicator, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-details.action.render', async (root, api) => { const draftOrderId = api.data.selected[0].id; let loading = false; let draftOrder = null; let sendEmail = true; let success = false; let error = false; let paymentLink = ''; const adminAction = root.createComponent(AdminAction, {title: 'Generate Payment Link'}); // Show loading state const loadingStack = root.createComponent(BlockStack); loadingStack.appendChild(root.createComponent(ProgressIndicator, {size: 'small-100'})); loadingStack.appendChild(root.createComponent(Text, {}, 'Loading draft order details...')); adminAction.appendChild(loadingStack); root.appendChild(adminAction); root.mount(); try { // Fetch draft order details from GraphQL Admin API const {data: draftOrderData} = await api.query( ` query GetDraftOrder($id: ID!) { draftOrder(id: $id) { id name customer { email firstName lastName } totalPriceSet { presentmentMoney { amount currencyCode } } } } `, {variables: {id: draftOrderId}} ); draftOrder = draftOrderData.draftOrder; } catch (err) { console.error('Error fetching draft order:', err); } const handleGenerate = async () => { loading = true; success = false; error = false; updateUI(); try { // Generate payment link through your app's backend const response = await fetch('https://your-app.com/api/generate-payment-link', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ draftOrderId, sendEmail, }), }); if (response.ok) { const {paymentUrl} = await response.json(); paymentLink = paymentUrl; success = true; updateUI(); } else { error = true; updateUI(); } } catch (err) { error = true; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); secondaryAction.replaceChildren(); if (success) { content.appendChild( root.createComponent( Banner, {tone: 'success', dismissible: true}, `Payment link generated successfully!${sendEmail ? ' Email sent to customer.' : ''}` ) ); } if (error) { content.appendChild( root.createComponent( Banner, {tone: 'critical', dismissible: true}, 'Failed to generate payment link. Please try again.' ) ); } // Draft order information const infoSection = root.createComponent(Section, {heading: 'Draft order information'}); const infoStack = root.createComponent(BlockStack); const orderBox = root.createComponent(Box); orderBox.appendChild(root.createComponent(Text, {fontWeight: 'bold'}, 'Draft order: ')); orderBox.appendChild(root.createComponent(Text, {}, draftOrder.name)); infoStack.appendChild(orderBox); if (draftOrder.customer) { const customerBox = root.createComponent(Box); customerBox.appendChild(root.createComponent(Text, {fontWeight: 'bold'}, 'Customer: ')); customerBox.appendChild( root.createComponent( Text, {}, `${draftOrder.customer.firstName} ${draftOrder.customer.lastName}` ) ); customerBox.appendChild( root.createComponent(Text, {tone: 'subdued'}, ` (${draftOrder.customer.email})`) ); infoStack.appendChild(customerBox); } const totalBox = root.createComponent(Box); totalBox.appendChild(root.createComponent(Text, {fontWeight: 'bold'}, 'Total: ')); totalBox.appendChild( root.createComponent( Text, {}, `${draftOrder.totalPriceSet.presentmentMoney.currencyCode} ${draftOrder.totalPriceSet.presentmentMoney.amount}` ) ); infoStack.appendChild(totalBox); infoSection.appendChild(infoStack); content.appendChild(infoSection); // Payment link options const optionsSection = root.createComponent(Section, {heading: 'Payment link options'}); const optionsStack = root.createComponent(BlockStack); optionsStack.appendChild( root.createComponent( Checkbox, { checked: sendEmail, onChange: (value) => { sendEmail = value; }, disabled: !draftOrder.customer?.email, }, 'Send payment link to customer via email' ) ); if (!draftOrder.customer?.email) { optionsStack.appendChild( root.createComponent(Text, {tone: 'subdued'}, 'No customer email available') ); } optionsSection.appendChild(optionsStack); content.appendChild(optionsSection); // Payment link section (if generated) if (paymentLink) { const linkSection = root.createComponent(Section, {heading: 'Payment link'}); const linkStack = root.createComponent(BlockStack); linkStack.appendChild( root.createComponent(TextField, { label: 'Payment URL', value: paymentLink, readOnly: true, }) ); linkStack.appendChild( root.createComponent( Button, { onPress: () => navigator.clipboard.writeText(paymentLink), variant: 'secondary', }, 'Copy to clipboard' ) ); linkSection.appendChild(linkStack); content.appendChild(linkSection); } primaryAction.appendChild( root.createComponent( Button, { onPress: handleGenerate, disabled: loading || success, }, loading ? 'Generating...' : 'Generate Link' ) ); secondaryAction.appendChild( root.createComponent( Button, {onPress: () => api.close()}, success ? 'Close' : 'Cancel' ) ); }; // Clear loading state and show form adminAction.replaceChildren(); updateUI(); adminAction.setProps({ title: 'Generate Payment Link', primaryAction, secondaryAction, }); adminAction.appendChild(content); } );Description
Add an action extension that applies wholesale pricing from an external system to a draft order. This example demonstrates how to fetch pricing rules and update draft order line items with custom wholesale prices.
React
import React from 'react'; import {useState, useEffect} from 'react'; import { reactExtension, useApi, AdminAction, Banner, Section, BlockStack, Box, Select, Text, Button, ProgressIndicator, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.draft-order-details.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 [pricingTiers, setPricingTiers] = useState([]); const [selectedTier, setSelectedTier] = useState(''); const [lineItems, setLineItems] = useState([]); const [success, setSuccess] = useState(false); const [error, setError] = useState(false); useEffect(() => { const fetchData = async () => { const draftOrderId = data.selected[0].id; try { // Fetch draft order line items const {data: draftOrderData} = await query( ` query GetDraftOrder($id: ID!) { draftOrder(id: $id) { id customer { id } lineItems(first: 50) { edges { node { id name quantity originalUnitPriceSet { presentmentMoney { amount } } } } } } } `, {variables: {id: draftOrderId}} ); const items = draftOrderData.draftOrder.lineItems.edges.map(edge => edge.node); setLineItems(items); // Fetch wholesale pricing tiers from your app's backend const customerId = draftOrderData.draftOrder.customer?.id; if (customerId) { const pricingResponse = await fetch( `https://your-app.com/api/wholesale-tiers?customerId=${customerId}` ); const {tiers} = await pricingResponse.json(); setPricingTiers( tiers.map(tier => ({value: tier.id, label: `${tier.name} (${tier.discount}% off)`})) ); if (tiers.length > 0) { setSelectedTier(tiers[0].id); } } } catch (err) { console.error('Error fetching data:', err); } finally { setFetching(false); } }; fetchData(); }, [data, query]); const handleApply = async () => { setLoading(true); setSuccess(false); setError(false); const draftOrderId = data.selected[0].id; try { // Apply wholesale pricing through your app's backend const response = await fetch('https://your-app.com/api/apply-wholesale-pricing', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ draftOrderId, tierId: selectedTier, }), }); if (response.ok) { setSuccess(true); setTimeout(() => { close(); // Reload the page to show updated prices window.location.reload(); }, 1500); } else { setError(true); } } catch (err) { setError(true); } finally { setLoading(false); } }; if (fetching) { return ( <AdminAction title="Apply Wholesale Pricing"> <BlockStack> <ProgressIndicator size="small-100" /> <Text>Loading pricing information...</Text> </BlockStack> </AdminAction> ); } if (pricingTiers.length === 0) { return ( <AdminAction title="Apply Wholesale Pricing" secondaryAction={ <Button onPress={close}> Close </Button> } > <Banner tone="info"> No wholesale pricing tiers available for this customer. </Banner> </AdminAction> ); } return ( <AdminAction title="Apply Wholesale Pricing" primaryAction={ <Button onPress={handleApply} disabled={loading || success || !selectedTier} > {loading ? 'Applying...' : 'Apply Pricing'} </Button> } secondaryAction={ <Button onPress={close}> Cancel </Button> } > {success && ( <Banner tone="success" dismissible> Wholesale pricing applied successfully! </Banner> )} {error && ( <Banner tone="critical" dismissible> Failed to apply wholesale pricing. Please try again. </Banner> )} <Section heading="Pricing tier"> <BlockStack> <Select label="Select wholesale tier" value={selectedTier} onChange={setSelectedTier} options={pricingTiers} /> <Box> <Text fontWeight="bold">Line items to update: </Text> <Text>{lineItems.length} items</Text> </Box> </BlockStack> </Section> <Banner tone="info"> This will update all line item prices according to the selected wholesale tier. </Banner> </AdminAction> ); }TS
import { extension, AdminAction, Banner, Section, BlockStack, Box, Select, Text, Button, ProgressIndicator, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-details.action.render', async (root, api) => { const draftOrderId = api.data.selected[0].id; let loading = false; let pricingTiers = []; let selectedTier = ''; let lineItems = []; let success = false; let error = false; const adminAction = root.createComponent(AdminAction, {title: 'Apply Wholesale Pricing'}); // Show loading state const loadingStack = root.createComponent(BlockStack); loadingStack.appendChild(root.createComponent(ProgressIndicator, {size: 'small-100'})); loadingStack.appendChild(root.createComponent(Text, {}, 'Loading pricing information...')); adminAction.appendChild(loadingStack); root.appendChild(adminAction); root.mount(); try { // Fetch draft order line items const {data: draftOrderData} = await api.query( ` query GetDraftOrder($id: ID!) { draftOrder(id: $id) { id customer { id } lineItems(first: 50) { edges { node { id name quantity originalUnitPriceSet { presentmentMoney { amount } } } } } } } `, {variables: {id: draftOrderId}} ); lineItems = draftOrderData.draftOrder.lineItems.edges.map(edge => edge.node); // Fetch wholesale pricing tiers from your app's backend const customerId = draftOrderData.draftOrder.customer?.id; if (customerId) { const pricingResponse = await fetch( `https://your-app.com/api/wholesale-tiers?customerId=${customerId}` ); const {tiers} = await pricingResponse.json(); pricingTiers = tiers.map(tier => ({ value: tier.id, label: `${tier.name} (${tier.discount}% off)`, })); if (tiers.length > 0) { selectedTier = tiers[0].id; } } } catch (err) { console.error('Error fetching data:', err); } // Show "no pricing tiers" state if none available if (pricingTiers.length === 0) { adminAction.replaceChildren(); adminAction.appendChild( root.createComponent(Banner, {tone: 'info'}, 'No wholesale pricing tiers available for this customer.') ); const secondaryActionFrag = root.createFragment(); secondaryActionFrag.appendChild( root.createComponent(Button, {onPress: () => api.close()}, 'Close') ); adminAction.setProps({ title: 'Apply Wholesale Pricing', secondaryAction: secondaryActionFrag, }); return; } const handleApply = async () => { loading = true; success = false; error = false; updateUI(); try { // Apply wholesale pricing through your app's backend const response = await fetch('https://your-app.com/api/apply-wholesale-pricing', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ draftOrderId, tierId: selectedTier, }), }); if (response.ok) { success = true; updateUI(); setTimeout(() => { api.close(); window.location.reload(); }, 1500); } else { error = true; updateUI(); } } catch (err) { error = true; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); secondaryAction.replaceChildren(); if (success) { content.appendChild( root.createComponent( Banner, {tone: 'success', dismissible: true}, 'Wholesale pricing applied successfully!' ) ); } if (error) { content.appendChild( root.createComponent( Banner, {tone: 'critical', dismissible: true}, 'Failed to apply wholesale pricing. Please try again.' ) ); } const tierSection = root.createComponent(Section, {heading: 'Pricing tier'}); const tierStack = root.createComponent(BlockStack); tierStack.appendChild( root.createComponent(Select, { label: 'Select wholesale tier', value: selectedTier, onChange: (value) => { selectedTier = value; }, options: pricingTiers, }) ); const itemsBox = root.createComponent(Box); itemsBox.appendChild(root.createComponent(Text, {fontWeight: 'bold'}, 'Line items to update: ')); itemsBox.appendChild(root.createComponent(Text, {}, `${lineItems.length} items`)); tierStack.appendChild(itemsBox); tierSection.appendChild(tierStack); content.appendChild(tierSection); content.appendChild( root.createComponent( Banner, {tone: 'info'}, 'This will update all line item prices according to the selected wholesale tier.' ) ); primaryAction.appendChild( root.createComponent( Button, { onPress: handleApply, disabled: loading || success || !selectedTier, }, loading ? 'Applying...' : 'Apply Pricing' ) ); secondaryAction.appendChild( root.createComponent(Button, {onPress: () => api.close()}, 'Cancel') ); }; // Clear loading state and show form adminAction.replaceChildren(); updateUI(); adminAction.setProps({ title: 'Apply Wholesale Pricing', primaryAction, secondaryAction, }); adminAction.appendChild(content); } );
Anchor to Draft order details action (should render) ,[object Object]Draft order details action (should render) target
admin.draft-order-details.action.should-render
Controls the render state of an admin action extension on the draft order details page. Use this target to conditionally show or hide your action extension based on the draft order's properties, such as status, customer type, or total amount.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension is evaluated each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Conditionally display an action only for draft orders that have a customer assigned. This example demonstrates how to check if a customer is associated with the draft order.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-details.action.should-render', async (root, api) => { const draftOrderId = api.data.selected[0].id; try { // Fetch draft order customer information const {data} = await api.query( ` query GetDraftOrder($id: ID!) { draftOrder(id: $id) { customer { id } } } `, {variables: {id: draftOrderId}} ); // Only show action if draft order has a customer return {render: !!data.draftOrder.customer}; } catch (err) { console.error('Error fetching draft order:', err); return {render: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-details.action.should-render', async (root, api) => { const draftOrderId = api.data.selected[0].id; try { // Fetch draft order customer information const {data} = await api.query( ` query GetDraftOrder($id: ID!) { draftOrder(id: $id) { customer { id } } } `, {variables: {id: draftOrderId}} ); // Only show action if draft order has a customer return {render: !!data.draftOrder.customer}; } catch (err) { console.error('Error fetching draft order:', err); return {render: false}; } } );Description
Conditionally display an action only for draft orders above a certain threshold. This example demonstrates filtering based on draft order total using the [GraphQL Admin API](/docs/api/admin-graphql).
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-details.action.should-render', async (root, api) => { const draftOrderId = api.data.selected[0].id; try { // Fetch draft order total const {data} = await api.query( ` query GetDraftOrder($id: ID!) { draftOrder(id: $id) { totalPriceSet { presentmentMoney { amount } } } } `, {variables: {id: draftOrderId}} ); const totalPrice = parseFloat(data.draftOrder.totalPriceSet.presentmentMoney.amount); // Only show action for draft orders over $1000 return {render: totalPrice > 1000}; } catch (err) { console.error('Error fetching draft order:', err); return {render: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-details.action.should-render', async (root, api) => { const draftOrderId = api.data.selected[0].id; try { // Fetch draft order total const {data} = await api.query( ` query GetDraftOrder($id: ID!) { draftOrder(id: $id) { totalPriceSet { presentmentMoney { amount } } } } `, {variables: {id: draftOrderId}} ); const totalPrice = parseFloat(data.draftOrder.totalPriceSet.presentmentMoney.amount); // Only show action for draft orders over $1000 return {render: totalPrice > 1000}; } catch (err) { console.error('Error fetching draft order:', err); return {render: false}; } } );
Anchor to Draft order details block ,[object Object]Draft order details block target
admin.draft-order-details.block.render
Renders an admin block extension inline on the draft order details page. Use this target to display contextual information, validation status, external system data, or payment status related to the draft order without requiring merchants to open a modal.
Extensions at this target appear as cards on the page and can show real-time data, insights, or quick actions. Blocks provide persistent visibility and are ideal for displaying 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 draft order status from an external order management system. This example demonstrates how to present order processing insights inline on the draft order page.
React
import React from 'react'; import {useState, useEffect} from 'react'; import { reactExtension, useApi, AdminBlock, BlockStack, Box, Badge, Heading, Text, Divider, Button, ProgressIndicator, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.draft-order-details.block.render'; export default reactExtension(TARGET, () => <App />); function App() { const {data} = useApi(TARGET); const [orderStatus, setOrderStatus] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const fetchOrderStatus = async () => { const draftOrderId = data.selected[0].id; try { // Fetch order status from your app's backend const response = await fetch( `https://your-app.com/api/draft-order-status?draftOrderId=${draftOrderId}` ); const statusData = await response.json(); setOrderStatus(statusData); } catch (err) { console.error('Error fetching order status:', err); } finally { setLoading(false); } }; fetchOrderStatus(); }, [data]); if (loading) { return ( <AdminBlock title="External Order Status"> <BlockStack> <ProgressIndicator size="small-100" /> <Text>Loading order status...</Text> </BlockStack> </AdminBlock> ); } if (!orderStatus) { return ( <AdminBlock title="External Order Status"> <Text tone="subdued">Not synced with external system</Text> </AdminBlock> ); } return ( <AdminBlock title="External Order Status"> <BlockStack> <Box> <Heading>Sync Status</Heading> <Badge tone={orderStatus.synced ? 'success' : 'warning'}> {orderStatus.synced ? 'Synced' : 'Pending'} </Badge> </Box> <Divider /> <Box> <Heading>External Order ID</Heading> <Text>{orderStatus.externalOrderId || 'Not assigned'}</Text> </Box> <Divider /> <Box> <Heading>Processing Status</Heading> <Text>{orderStatus.processingStatus}</Text> </Box> {orderStatus.warehouse && ( <> <Divider /> <Box> <Heading>Assigned Warehouse</Heading> <Text>{orderStatus.warehouse}</Text> </Box> </> )} {orderStatus.lastSyncedAt && ( <> <Divider /> <Box> <Heading>Last Synced</Heading> <Text tone="subdued">{orderStatus.lastSyncedAt}</Text> </Box> </> )} {orderStatus.externalUrl && ( <Button onPress={() => window.open(orderStatus.externalUrl, '_blank')} variant="secondary" > View in external system </Button> )} </BlockStack> </AdminBlock> ); }TS
import { extension, AdminBlock, BlockStack, Box, Badge, Heading, Text, Divider, Button, ProgressIndicator, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-details.block.render', async (root, api) => { const draftOrderId = api.data.selected[0].id; const adminBlock = root.createComponent(AdminBlock, {title: 'External Order Status'}); // Show loading state const loadingStack = root.createComponent(BlockStack); loadingStack.appendChild(root.createComponent(ProgressIndicator, {size: 'small-100'})); loadingStack.appendChild(root.createComponent(Text, {}, 'Loading order status...')); adminBlock.appendChild(loadingStack); root.appendChild(adminBlock); root.mount(); try { // Fetch order status from your app's backend const response = await fetch( `https://your-app.com/api/draft-order-status?draftOrderId=${draftOrderId}` ); const orderStatus = await response.json(); // Clear loading state adminBlock.replaceChildren(); if (!orderStatus) { adminBlock.appendChild( root.createComponent(Text, {tone: 'subdued'}, 'Not synced with external system') ); return; } const blockStack = root.createComponent(BlockStack); // Sync Status const syncBox = root.createComponent(Box); syncBox.appendChild(root.createComponent(Heading, {}, 'Sync Status')); syncBox.appendChild( root.createComponent( Badge, {tone: orderStatus.synced ? 'success' : 'warning'}, orderStatus.synced ? 'Synced' : 'Pending' ) ); blockStack.appendChild(syncBox); blockStack.appendChild(root.createComponent(Divider)); // External Order ID const orderIdBox = root.createComponent(Box); orderIdBox.appendChild(root.createComponent(Heading, {}, 'External Order ID')); orderIdBox.appendChild( root.createComponent(Text, {}, orderStatus.externalOrderId || 'Not assigned') ); blockStack.appendChild(orderIdBox); blockStack.appendChild(root.createComponent(Divider)); // Processing Status const statusBox = root.createComponent(Box); statusBox.appendChild(root.createComponent(Heading, {}, 'Processing Status')); statusBox.appendChild(root.createComponent(Text, {}, orderStatus.processingStatus)); blockStack.appendChild(statusBox); // Warehouse (if present) if (orderStatus.warehouse) { blockStack.appendChild(root.createComponent(Divider)); const warehouseBox = root.createComponent(Box); warehouseBox.appendChild(root.createComponent(Heading, {}, 'Assigned Warehouse')); warehouseBox.appendChild(root.createComponent(Text, {}, orderStatus.warehouse)); blockStack.appendChild(warehouseBox); } // Last Synced (if present) if (orderStatus.lastSyncedAt) { blockStack.appendChild(root.createComponent(Divider)); const syncedBox = root.createComponent(Box); syncedBox.appendChild(root.createComponent(Heading, {}, 'Last Synced')); syncedBox.appendChild( root.createComponent(Text, {tone: 'subdued'}, orderStatus.lastSyncedAt) ); blockStack.appendChild(syncedBox); } // External URL button (if present) if (orderStatus.externalUrl) { blockStack.appendChild( root.createComponent( Button, { onPress: () => window.open(orderStatus.externalUrl, '_blank'), variant: 'secondary', }, 'View in external system' ) ); } adminBlock.appendChild(blockStack); } catch (err) { console.error('Error fetching order status:', err); adminBlock.replaceChildren( root.createComponent(Text, {tone: 'subdued'}, 'Not synced with external system') ); } } );Description
Create a block extension that shows payment request status for a draft order. This example demonstrates how to display payment link information and payment status from an external payment gateway.
React
import React from 'react'; import {useState, useEffect} from 'react'; import { reactExtension, useApi, AdminBlock, BlockStack, Box, Badge, Heading, Text, Divider, Button, ProgressIndicator, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.draft-order-details.block.render'; export default reactExtension(TARGET, () => <App />); function App() { const {data} = useApi(TARGET); const [paymentData, setPaymentData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const fetchPaymentData = async () => { const draftOrderId = data.selected[0].id; try { // Fetch payment data from your app's backend const response = await fetch( `https://your-app.com/api/payment-status?draftOrderId=${draftOrderId}` ); const paymentInfo = await response.json(); setPaymentData(paymentInfo); } catch (err) { console.error('Error fetching payment data:', err); } finally { setLoading(false); } }; fetchPaymentData(); }, [data]); if (loading) { return ( <AdminBlock title="Payment Status"> <ProgressIndicator size="small-100" /> </AdminBlock> ); } if (!paymentData || !paymentData.paymentLinkSent) { return ( <AdminBlock title="Payment Status"> <Text tone="subdued">No payment request sent</Text> </AdminBlock> ); } return ( <AdminBlock title="Payment Status"> <BlockStack> <Box> <Heading>Payment Link Status</Heading> <Badge tone={paymentData.status === 'paid' ? 'success' : 'info'}> {paymentData.status === 'paid' ? 'Paid' : 'Pending Payment'} </Badge> </Box> <Divider /> <Box> <Heading>Sent To</Heading> <Text>{paymentData.customerEmail}</Text> </Box> <Divider /> <Box> <Heading>Amount Due</Heading> <Text fontWeight="bold"> {paymentData.currency} {paymentData.amountDue} </Text> </Box> {paymentData.status === 'paid' && paymentData.paidAt && ( <> <Divider /> <Box> <Heading>Paid On</Heading> <Text tone="subdued">{paymentData.paidAt}</Text> </Box> </> )} {paymentData.status !== 'paid' && paymentData.linkExpiresAt && ( <> <Divider /> <Box> <Heading>Link Expires</Heading> <Text tone="subdued">{paymentData.linkExpiresAt}</Text> </Box> </> )} {paymentData.status !== 'paid' && ( <Box> <Heading>Payment Reminders Sent</Heading> <Text>{paymentData.remindersSent || 0}</Text> </Box> )} {paymentData.paymentUrl && paymentData.status !== 'paid' && ( <Button onPress={() => window.open(paymentData.paymentUrl, '_blank')} variant="secondary" > View payment page </Button> )} </BlockStack> </AdminBlock> ); }TS
import { extension, AdminBlock, BlockStack, Box, Badge, Heading, Text, Divider, Button, ProgressIndicator, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-details.block.render', async (root, api) => { const draftOrderId = api.data.selected[0].id; const adminBlock = root.createComponent(AdminBlock, {title: 'Payment Status'}); // Show loading state adminBlock.appendChild(root.createComponent(ProgressIndicator, {size: 'small-100'})); root.appendChild(adminBlock); root.mount(); try { // Fetch payment data from your app's backend const response = await fetch( `https://your-app.com/api/payment-status?draftOrderId=${draftOrderId}` ); const paymentData = await response.json(); // Clear loading state adminBlock.replaceChildren(); if (!paymentData || !paymentData.paymentLinkSent) { adminBlock.appendChild( root.createComponent(Text, {tone: 'subdued'}, 'No payment request sent') ); return; } const blockStack = root.createComponent(BlockStack); // Payment Link Status const statusBox = root.createComponent(Box); statusBox.appendChild(root.createComponent(Heading, {}, 'Payment Link Status')); statusBox.appendChild( root.createComponent( Badge, {tone: paymentData.status === 'paid' ? 'success' : 'info'}, paymentData.status === 'paid' ? 'Paid' : 'Pending Payment' ) ); blockStack.appendChild(statusBox); blockStack.appendChild(root.createComponent(Divider)); // Sent To const sentToBox = root.createComponent(Box); sentToBox.appendChild(root.createComponent(Heading, {}, 'Sent To')); sentToBox.appendChild(root.createComponent(Text, {}, paymentData.customerEmail)); blockStack.appendChild(sentToBox); blockStack.appendChild(root.createComponent(Divider)); // Amount Due const amountBox = root.createComponent(Box); amountBox.appendChild(root.createComponent(Heading, {}, 'Amount Due')); amountBox.appendChild( root.createComponent( Text, {fontWeight: 'bold'}, `${paymentData.currency} ${paymentData.amountDue}` ) ); blockStack.appendChild(amountBox); // Paid On (if paid) if (paymentData.status === 'paid' && paymentData.paidAt) { blockStack.appendChild(root.createComponent(Divider)); const paidBox = root.createComponent(Box); paidBox.appendChild(root.createComponent(Heading, {}, 'Paid On')); paidBox.appendChild(root.createComponent(Text, {tone: 'subdued'}, paymentData.paidAt)); blockStack.appendChild(paidBox); } // Link Expires (if not paid) if (paymentData.status !== 'paid' && paymentData.linkExpiresAt) { blockStack.appendChild(root.createComponent(Divider)); const expiresBox = root.createComponent(Box); expiresBox.appendChild(root.createComponent(Heading, {}, 'Link Expires')); expiresBox.appendChild( root.createComponent(Text, {tone: 'subdued'}, paymentData.linkExpiresAt) ); blockStack.appendChild(expiresBox); } // Payment Reminders (if not paid) if (paymentData.status !== 'paid') { const remindersBox = root.createComponent(Box); remindersBox.appendChild(root.createComponent(Heading, {}, 'Payment Reminders Sent')); remindersBox.appendChild( root.createComponent(Text, {}, String(paymentData.remindersSent || 0)) ); blockStack.appendChild(remindersBox); } // View payment page button (if not paid) if (paymentData.paymentUrl && paymentData.status !== 'paid') { blockStack.appendChild( root.createComponent( Button, { onPress: () => window.open(paymentData.paymentUrl, '_blank'), variant: 'secondary', }, 'View payment page' ) ); } adminBlock.appendChild(blockStack); } catch (err) { console.error('Error fetching payment data:', err); adminBlock.replaceChildren( root.createComponent(Text, {tone: 'subdued'}, 'No payment request sent') ); } } );
Anchor to Draft order index targetsDraft order index targets
Use action targets to extend the draft order index page with bulk operations and workflows that help merchants manage multiple draft orders efficiently.
Anchor to Draft order index action ,[object Object]Draft order index action target
admin.draft-order-index.action.render
Renders an admin action extension on the draft order index page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on the draft order list, such as batch processing, bulk exports, or generating reports.
Extensions at this target can access the page context through the Action Extension API. The action renders in a modal overlay, providing space for configuration and execution of list-wide operations.
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 exports all draft orders to an external ERP system. This example shows how to create a workflow that initiates a background sync job for draft orders.
React
import React from 'react'; import {useState} from 'react'; import { reactExtension, useApi, AdminAction, Banner, Section, BlockStack, Select, Text, Button, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.draft-order-index.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const {close} = useApi(TARGET); const [loading, setLoading] = useState(false); const [exportType, setExportType] = useState('all'); const [dateRange, setDateRange] = useState('7days'); const [success, setSuccess] = useState(false); const [error, setError] = useState(false); const exportTypes = [ {value: 'all', label: 'All draft orders'}, {value: 'open', label: 'Open draft orders only'}, {value: 'completed', label: 'Completed draft orders only'}, {value: 'invoiced', label: 'Invoiced draft orders only'}, ]; const dateRanges = [ {value: '7days', label: 'Last 7 days'}, {value: '30days', label: 'Last 30 days'}, {value: '90days', label: 'Last 90 days'}, {value: '1year', label: 'Last year'}, {value: 'all', label: 'All time'}, ]; const handleExport = async () => { setLoading(true); setSuccess(false); setError(false); try { // Export draft orders through your app's backend const response = await fetch('https://your-app.com/api/export-draft-orders', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ exportType, dateRange, }), }); if (response.ok) { setSuccess(true); close(); } else { setError(true); } } catch (err) { setError(true); } finally { setLoading(false); } }; return ( <AdminAction title="Export to ERP System" primaryAction={ <Button onPress={handleExport} disabled={loading || success} > {loading ? 'Starting Export...' : 'Start Export'} </Button> } secondaryAction={ <Button onPress={close}> Cancel </Button> } > {success && ( <Banner tone="success" dismissible> Draft order export initiated! You'll receive an email when complete. </Banner> )} {error && ( <Banner tone="critical" dismissible> Failed to initiate export. Please try again. </Banner> )} <Section heading="Export settings"> <BlockStack> <Select label="Export type" value={exportType} onChange={setExportType} options={exportTypes} /> <Select label="Date range" value={dateRange} onChange={setDateRange} options={dateRanges} /> <Banner tone="info"> This will export all matching draft orders to your ERP system. Large exports may take several minutes to complete. </Banner> </BlockStack> </Section> </AdminAction> ); }TS
import { extension, AdminAction, Banner, Section, BlockStack, Select, Button, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.action.render', (root, api) => { let loading = false; let exportType = 'all'; let dateRange = '7days'; let success = false; let error = false; const exportTypes = [ {value: 'all', label: 'All draft orders'}, {value: 'open', label: 'Open draft orders only'}, {value: 'completed', label: 'Completed draft orders only'}, {value: 'invoiced', label: 'Invoiced draft orders only'}, ]; const dateRanges = [ {value: '7days', label: 'Last 7 days'}, {value: '30days', label: 'Last 30 days'}, {value: '90days', label: 'Last 90 days'}, {value: '1year', label: 'Last year'}, {value: 'all', label: 'All time'}, ]; const handleExport = async () => { loading = true; success = false; error = false; updateUI(); try { // Export draft orders through your app's backend const response = await fetch('https://your-app.com/api/export-draft-orders', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ exportType, dateRange, }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { error = true; updateUI(); } } catch (err) { error = true; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); secondaryAction.replaceChildren(); if (success) { content.appendChild( root.createComponent( Banner, {tone: 'success', dismissible: true}, "Draft order export initiated! You'll receive an email when complete." ) ); } if (error) { content.appendChild( root.createComponent( Banner, {tone: 'critical', dismissible: true}, 'Failed to initiate export. Please try again.' ) ); } const section = root.createComponent(Section, {heading: 'Export settings'}); const blockStack = root.createComponent(BlockStack); blockStack.appendChild( root.createComponent(Select, { label: 'Export type', value: exportType, onChange: (value) => { exportType = value; }, options: exportTypes, }) ); blockStack.appendChild( root.createComponent(Select, { label: 'Date range', value: dateRange, onChange: (value) => { dateRange = value; }, options: dateRanges, }) ); blockStack.appendChild( root.createComponent( Banner, {tone: 'info'}, 'This will export all matching draft orders to your ERP system. Large exports may take several minutes to complete.' ) ); section.appendChild(blockStack); content.appendChild(section); primaryAction.appendChild( root.createComponent( Button, { onPress: handleExport, disabled: loading || success, }, loading ? 'Starting Export...' : 'Start Export' ) ); 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 invoices for multiple draft orders. This example shows how to create a workflow that processes draft orders and generates PDF invoices.
React
import React from 'react'; import {useState} from 'react'; import { reactExtension, useApi, AdminAction, Banner, Section, BlockStack, Select, Checkbox, Text, Button, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.draft-order-index.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const {close} = useApi(TARGET); const [loading, setLoading] = useState(false); const [invoiceFormat, setInvoiceFormat] = useState('pdf'); const [includePaymentTerms, setIncludePaymentTerms] = useState(true); const [success, setSuccess] = useState(false); const [error, setError] = useState(false); const formats = [ {value: 'pdf', label: 'PDF'}, {value: 'html', label: 'HTML'}, {value: 'csv', label: 'CSV (Summary)'}, ]; const handleGenerate = async () => { setLoading(true); setSuccess(false); setError(false); try { // Generate invoices through your app's backend const response = await fetch('https://your-app.com/api/generate-invoices', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ format: invoiceFormat, includePaymentTerms, }), }); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `invoices-${Date.now()}.zip`; a.click(); setSuccess(true); close(); } else { setError(true); } } catch (err) { setError(true); } finally { setLoading(false); } }; return ( <AdminAction title="Generate Invoices" primaryAction={ <Button onPress={handleGenerate} disabled={loading || success} > {loading ? 'Generating...' : 'Generate Invoices'} </Button> } secondaryAction={ <Button onPress={close}> Cancel </Button> } > {success && ( <Banner tone="success" dismissible> Invoices generated successfully! </Banner> )} {error && ( <Banner tone="critical" dismissible> Failed to generate invoices. Please try again. </Banner> )} <Section heading="Invoice settings"> <BlockStack> <Select label="Invoice format" value={invoiceFormat} onChange={setInvoiceFormat} options={formats} /> <Checkbox checked={includePaymentTerms} onChange={setIncludePaymentTerms} > Include payment terms and due dates </Checkbox> <Text tone="subdued"> Invoices will be generated for all open draft orders with customers. </Text> </BlockStack> </Section> </AdminAction> ); }TS
import { extension, AdminAction, Banner, Section, BlockStack, Select, Checkbox, Text, Button, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.action.render', (root, api) => { let loading = false; let invoiceFormat = 'pdf'; let includePaymentTerms = true; let success = false; let error = false; const formats = [ {value: 'pdf', label: 'PDF'}, {value: 'html', label: 'HTML'}, {value: 'csv', label: 'CSV (Summary)'}, ]; const handleGenerate = async () => { loading = true; success = false; error = false; updateUI(); try { // Generate invoices through your app's backend const response = await fetch('https://your-app.com/api/generate-invoices', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ format: invoiceFormat, includePaymentTerms, }), }); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `invoices-${Date.now()}.zip`; a.click(); success = true; updateUI(); api.close(); } else { error = true; updateUI(); } } catch (err) { error = true; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); secondaryAction.replaceChildren(); if (success) { content.appendChild( root.createComponent( Banner, {tone: 'success', dismissible: true}, 'Invoices generated successfully!' ) ); } if (error) { content.appendChild( root.createComponent( Banner, {tone: 'critical', dismissible: true}, 'Failed to generate invoices. Please try again.' ) ); } const section = root.createComponent(Section, {heading: 'Invoice settings'}); const blockStack = root.createComponent(BlockStack); blockStack.appendChild( root.createComponent(Select, { label: 'Invoice format', value: invoiceFormat, onChange: (value) => { invoiceFormat = value; }, options: formats, }) ); blockStack.appendChild( root.createComponent( Checkbox, { checked: includePaymentTerms, onChange: (value) => { includePaymentTerms = value; }, }, 'Include payment terms and due dates' ) ); blockStack.appendChild( root.createComponent( Text, {tone: 'subdued'}, 'Invoices will be generated for all open draft orders with customers.' ) ); section.appendChild(blockStack); content.appendChild(section); primaryAction.appendChild( root.createComponent( Button, { onPress: handleGenerate, disabled: loading || success, }, loading ? 'Generating...' : 'Generate Invoices' ) ); secondaryAction.appendChild( root.createComponent(Button, {onPress: () => api.close()}, 'Cancel') ); }; updateUI(); const adminAction = root.createComponent( AdminAction, { title: 'Generate Invoices', primaryAction, secondaryAction, } ); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
Anchor to Draft order index action (should render) ,[object Object]Draft order index action (should render) target
admin.draft-order-index.action.should-render
Controls the render state of an admin action extension on the draft order index page. Use this target to conditionally show or hide your action extension based on business logic, user permissions, or app configuration.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension is evaluated each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Conditionally display an action only when the app is properly configured. This example demonstrates how to check configuration status before showing the extension.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.action.should-render', async (root, api) => { try { // Check if app is configured through your backend const response = await fetch( 'https://your-app.com/api/check-configuration' ); const {configured, erpConnected} = await response.json(); // Only show action if app is configured and ERP is connected return {render: configured && erpConnected}; } catch (err) { console.error('Error checking configuration:', err); return {render: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.action.should-render', async (root, api) => { try { // Check if app is configured through your backend const response = await fetch( 'https://your-app.com/api/check-configuration' ); const {configured, erpConnected} = await response.json(); // Only show action if app is configured and ERP is connected return {render: configured && erpConnected}; } catch (err) { console.error('Error checking configuration:', err); return {render: false}; } } );Description
Conditionally display an action only when the merchant's subscription plan includes advanced draft order features. This example demonstrates checking plan entitlements.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.action.should-render', async (root, api) => { try { // Check plan features through your app's backend const response = await fetch( 'https://your-app.com/api/check-plan-features' ); const {features} = await response.json(); // Only show action if bulk operations feature is available return {render: features.includes('bulk-draft-order-operations')}; } catch (err) { console.error('Error checking plan features:', err); return {render: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.action.should-render', async (root, api) => { try { // Check plan features through your app's backend const response = await fetch( 'https://your-app.com/api/check-plan-features' ); const {features} = await response.json(); // Only show action if bulk operations feature is available return {render: features.includes('bulk-draft-order-operations')}; } catch (err) { console.error('Error checking plan features:', err); return {render: false}; } } );
Anchor to Draft order index selection action ,[object Object]Draft order index selection action target
admin.draft-order-index.selection-action.render
Renders a selection action extension on the draft order index page when multiple draft orders are selected. Merchants can access this extension from the More actions menu of the resource list. Use this target to provide bulk operations on selected draft orders, such as bulk conversion, batch invoice generation, or bulk status updates.
Extensions at this target can access the IDs of selected draft orders through the data property in the Action Extension API. The action renders in a modal overlay designed for batch processing.
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 sends payment requests for multiple draft orders. This example shows how to process multiple draft order IDs and generate payment links for each.
React
import React from 'react'; import {useState, useEffect} from 'react'; import { reactExtension, useApi, AdminAction, Banner, Section, BlockStack, Box, Select, Text, Button, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.draft-order-index.selection-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const {data, close} = useApi(TARGET); const [loading, setLoading] = useState(false); const [draftOrderCount, setDraftOrderCount] = useState(0); const [sendMethod, setSendMethod] = useState('email'); const [emailTemplate, setEmailTemplate] = useState('default'); const [success, setSuccess] = useState(false); const [error, setError] = useState(false); const [eligibleCount, setEligibleCount] = useState(0); const sendMethods = [ {value: 'email', label: 'Email'}, {value: 'sms', label: 'SMS (if available)'}, ]; const emailTemplates = [ {value: 'default', label: 'Default payment request'}, {value: 'friendly', label: 'Friendly reminder'}, {value: 'professional', label: 'Professional invoice'}, {value: 'urgent', label: 'Urgent payment request'}, ]; useEffect(() => { const checkEligibility = async () => { const selectedDraftOrders = data.selected || []; setDraftOrderCount(selectedDraftOrders.length); // Check how many draft orders have customers with email addresses try { const draftOrderIds = selectedDraftOrders.map((draftOrder) => draftOrder.id); const response = await fetch('https://your-app.com/api/check-payment-eligibility', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({draftOrderIds}), }); const {eligibleCount: count} = await response.json(); setEligibleCount(count); } catch (err) { console.error('Error checking eligibility:', err); } }; checkEligibility(); }, [data]); const handleSend = async () => { setLoading(true); setSuccess(false); setError(false); const draftOrderIds = data.selected.map((draftOrder) => draftOrder.id); try { // Send payment requests through your app's backend const response = await fetch('https://your-app.com/api/send-payment-requests', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ draftOrderIds, sendMethod, emailTemplate, }), }); if (response.ok) { setSuccess(true); close(); } else { setError(true); } } catch (err) { setError(true); } finally { setLoading(false); } }; return ( <AdminAction title="Send Payment Requests" primaryAction={ <Button onPress={handleSend} disabled={loading || success || eligibleCount === 0} > {loading ? 'Sending...' : `Send ${eligibleCount} Payment Requests`} </Button> } secondaryAction={ <Button onPress={close}> Cancel </Button> } > {success && ( <Banner tone="success" dismissible> Payment requests sent successfully to {eligibleCount} customers! </Banner> )} {error && ( <Banner tone="critical" dismissible> Failed to send payment requests. Please try again. </Banner> )} <Section heading="Payment request settings"> <BlockStack> <Box> <Text fontWeight="bold">Selected draft orders: </Text> <Text>{draftOrderCount}</Text> </Box> <Box> <Text fontWeight="bold">Eligible for payment requests: </Text> <Text>{eligibleCount}</Text> <Text tone="subdued"> (with customer email)</Text> </Box> <Select label="Delivery method" value={sendMethod} onChange={setSendMethod} options={sendMethods} /> <Select label="Email template" value={emailTemplate} onChange={setEmailTemplate} options={emailTemplates} /> {eligibleCount < draftOrderCount && ( <Banner tone="warning"> {draftOrderCount - eligibleCount} draft orders will be skipped (missing customer email). </Banner> )} </BlockStack> </Section> </AdminAction> ); }TS
import { extension, AdminAction, Banner, Section, BlockStack, Box, Select, Text, Button, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.selection-action.render', async (root, api) => { const selectedDraftOrders = api.data.selected || []; const draftOrderCount = selectedDraftOrders.length; let loading = false; let sendMethod = 'email'; let emailTemplate = 'default'; let success = false; let error = false; let eligibleCount = 0; const sendMethods = [ {value: 'email', label: 'Email'}, {value: 'sms', label: 'SMS (if available)'}, ]; const emailTemplates = [ {value: 'default', label: 'Default payment request'}, {value: 'friendly', label: 'Friendly reminder'}, {value: 'professional', label: 'Professional invoice'}, {value: 'urgent', label: 'Urgent payment request'}, ]; const adminAction = root.createComponent(AdminAction, { title: 'Send Payment Requests', }); root.appendChild(adminAction); root.mount(); // Check how many draft orders have customers with email addresses try { const draftOrderIds = selectedDraftOrders.map((draftOrder) => draftOrder.id); const response = await fetch('https://your-app.com/api/check-payment-eligibility', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({draftOrderIds}), }); const {eligibleCount: count} = await response.json(); eligibleCount = count; } catch (err) { console.error('Error checking eligibility:', err); } const handleSend = async () => { loading = true; success = false; error = false; updateUI(); const draftOrderIds = api.data.selected.map((draftOrder) => draftOrder.id); try { // Send payment requests through your app's backend const response = await fetch('https://your-app.com/api/send-payment-requests', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ draftOrderIds, sendMethod, emailTemplate, }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { error = true; updateUI(); } } catch (err) { error = true; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); secondaryAction.replaceChildren(); if (success) { content.appendChild( root.createComponent( Banner, {tone: 'success', dismissible: true}, `Payment requests sent successfully to ${eligibleCount} customers!` ) ); } if (error) { content.appendChild( root.createComponent( Banner, {tone: 'critical', dismissible: true}, 'Failed to send payment requests. Please try again.' ) ); } const section = root.createComponent(Section, {heading: 'Payment request settings'}); const blockStack = root.createComponent(BlockStack); const selectedBox = root.createComponent(Box); selectedBox.appendChild( root.createComponent(Text, {fontWeight: 'bold'}, 'Selected draft orders: ') ); selectedBox.appendChild(root.createComponent(Text, {}, String(draftOrderCount))); blockStack.appendChild(selectedBox); const eligibleBox = root.createComponent(Box); eligibleBox.appendChild( root.createComponent(Text, {fontWeight: 'bold'}, 'Eligible for payment requests: ') ); eligibleBox.appendChild(root.createComponent(Text, {}, String(eligibleCount))); eligibleBox.appendChild( root.createComponent(Text, {tone: 'subdued'}, ' (with customer email)') ); blockStack.appendChild(eligibleBox); blockStack.appendChild( root.createComponent(Select, { label: 'Delivery method', value: sendMethod, onChange: (value) => { sendMethod = value; }, options: sendMethods, }) ); blockStack.appendChild( root.createComponent(Select, { label: 'Email template', value: emailTemplate, onChange: (value) => { emailTemplate = value; }, options: emailTemplates, }) ); if (eligibleCount < draftOrderCount) { blockStack.appendChild( root.createComponent( Banner, {tone: 'warning'}, `${draftOrderCount - eligibleCount} draft orders will be skipped (missing customer email).` ) ); } section.appendChild(blockStack); content.appendChild(section); primaryAction.appendChild( root.createComponent( Button, { onPress: handleSend, disabled: loading || success || eligibleCount === 0, }, loading ? 'Sending...' : `Send ${eligibleCount} Payment Requests` ) ); secondaryAction.appendChild( root.createComponent(Button, {onPress: () => api.close()}, 'Cancel') ); }; updateUI(); adminAction.setProps({ title: 'Send Payment Requests', primaryAction, secondaryAction, }); adminAction.appendChild(content); } );Description
Add a selection action extension that converts multiple draft orders to orders at once. This example demonstrates bulk operations with confirmation and progress tracking.
React
import React from 'react'; import {useState} from 'react'; import { reactExtension, useApi, AdminAction, Banner, Section, BlockStack, Checkbox, Text, Button, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.draft-order-index.selection-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const {data, close} = useApi(TARGET); const [loading, setLoading] = useState(false); const [sendInvoices, setSendInvoices] = useState(true); const [markAsPaid, setMarkAsPaid] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(false); const draftOrderCount = data.selected?.length || 0; const handleConvert = async () => { setLoading(true); setSuccess(false); setError(false); const draftOrderIds = data.selected.map((draftOrder) => draftOrder.id); try { // Convert draft orders through your app's backend const response = await fetch('https://your-app.com/api/bulk-convert-draft-orders', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ draftOrderIds, sendInvoices, markAsPaid, }), }); if (response.ok) { setSuccess(true); setTimeout(() => { close(); // Reload the page to show updated list window.location.reload(); }, 1500); } else { setError(true); } } catch (err) { setError(true); } finally { setLoading(false); } }; return ( <AdminAction title="Bulk Convert to Orders" primaryAction={ <Button onPress={handleConvert} disabled={loading || success} > {loading ? 'Converting...' : `Convert ${draftOrderCount} Orders`} </Button> } secondaryAction={ <Button onPress={close}> Cancel </Button> } > {success && ( <Banner tone="success" dismissible> {draftOrderCount} draft orders converted successfully! </Banner> )} {error && ( <Banner tone="critical" dismissible> Failed to convert draft orders. Please try again. </Banner> )} <Section heading="Conversion settings"> <BlockStack> <Text> Converting {draftOrderCount} selected draft order{draftOrderCount !== 1 ? 's' : ''} to orders </Text> <Checkbox checked={sendInvoices} onChange={setSendInvoices} > Send invoice emails to customers </Checkbox> <Checkbox checked={markAsPaid} onChange={setMarkAsPaid} > Mark orders as paid </Checkbox> <Banner tone="info"> This action will convert all selected draft orders to orders. This operation cannot be undone. </Banner> </BlockStack> </Section> </AdminAction> ); }TS
import { extension, AdminAction, Banner, Section, BlockStack, Checkbox, Text, Button, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.selection-action.render', (root, api) => { const draftOrderCount = api.data.selected?.length || 0; let loading = false; let sendInvoices = true; let markAsPaid = false; let success = false; let error = false; const handleConvert = async () => { loading = true; success = false; error = false; updateUI(); const draftOrderIds = api.data.selected.map((draftOrder) => draftOrder.id); try { // Convert draft orders through your app's backend const response = await fetch('https://your-app.com/api/bulk-convert-draft-orders', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ draftOrderIds, sendInvoices, markAsPaid, }), }); if (response.ok) { success = true; updateUI(); setTimeout(() => { api.close(); window.location.reload(); }, 1500); } else { error = true; updateUI(); } } catch (err) { error = true; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); secondaryAction.replaceChildren(); if (success) { content.appendChild( root.createComponent( Banner, {tone: 'success', dismissible: true}, `${draftOrderCount} draft orders converted successfully!` ) ); } if (error) { content.appendChild( root.createComponent( Banner, {tone: 'critical', dismissible: true}, 'Failed to convert draft orders. Please try again.' ) ); } const section = root.createComponent(Section, {heading: 'Conversion settings'}); const blockStack = root.createComponent(BlockStack); blockStack.appendChild( root.createComponent( Text, {}, `Converting ${draftOrderCount} selected draft order${draftOrderCount !== 1 ? 's' : ''} to orders` ) ); blockStack.appendChild( root.createComponent( Checkbox, { checked: sendInvoices, onChange: (value) => { sendInvoices = value; }, }, 'Send invoice emails to customers' ) ); blockStack.appendChild( root.createComponent( Checkbox, { checked: markAsPaid, onChange: (value) => { markAsPaid = value; }, }, 'Mark orders as paid' ) ); blockStack.appendChild( root.createComponent( Banner, {tone: 'info'}, 'This action will convert all selected draft orders to orders. This operation cannot be undone.' ) ); section.appendChild(blockStack); content.appendChild(section); primaryAction.appendChild( root.createComponent( Button, { onPress: handleConvert, disabled: loading || success, }, loading ? 'Converting...' : `Convert ${draftOrderCount} Orders` ) ); secondaryAction.appendChild( root.createComponent(Button, {onPress: () => api.close()}, 'Cancel') ); }; updateUI(); const adminAction = root.createComponent( AdminAction, { title: 'Bulk Convert to Orders', primaryAction, secondaryAction, } ); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
Anchor to Draft order index selection action (should render) ,[object Object]Draft order index selection action (should render) target
admin.draft-order-index.selection-action.should-render
Controls the render state of a selection action extension on the draft order index page when multiple draft orders are selected. Use this target to conditionally show or hide your bulk action extension based on the number of selected draft orders, their properties, or app configuration.
This target returns a boolean value that determines whether the corresponding selection action extension appears in the More actions menu. The extension is evaluated each time the selection changes.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Conditionally display a bulk action only when a reasonable number of draft orders are selected. This example demonstrates how to limit bulk operations based on selection size.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.selection-action.should-render', async (root, api) => { const selectedCount = api.data.selected?.length || 0; // Only show action if between 1 and 50 draft orders are selected return {render: selectedCount > 0 && selectedCount <= 50}; } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.selection-action.should-render', async (root, api) => { const selectedCount = api.data.selected?.length || 0; // Only show action if between 1 and 50 draft orders are selected return {render: selectedCount > 0 && selectedCount <= 50}; } );Description
Conditionally display a bulk action based on whether selected draft orders meet specific criteria. This example demonstrates checking draft order properties before showing the extension.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.selection-action.should-render', async (root, api) => { const selectedIds = api.data.selected?.map((draftOrder) => draftOrder.id) || []; const selectedCount = selectedIds.length; // Don't show for empty selections if (selectedCount === 0) { return {render: false}; } try { // Check if selected draft orders are eligible for the action const response = await fetch( 'https://your-app.com/api/check-eligible-draft-orders', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({draftOrderIds: selectedIds}), } ); const {eligibleCount} = await response.json(); // Only show action if at least one selected draft order is eligible return {render: eligibleCount > 0}; } catch (err) { console.error('Error checking eligibility:', err); return {render: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.draft-order-index.selection-action.should-render', async (root, api) => { const selectedIds = api.data.selected?.map((draftOrder) => draftOrder.id) || []; const selectedCount = selectedIds.length; // Don't show for empty selections if (selectedCount === 0) { return {render: false}; } try { // Check if selected draft orders are eligible for the action const response = await fetch( 'https://your-app.com/api/check-eligible-draft-orders', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({draftOrderIds: selectedIds}), } ); const {eligibleCount} = await response.json(); // Only show action if at least one selected draft order is eligible return {render: eligibleCount > 0}; } catch (err) { console.error('Error checking eligibility:', err); return {render: false}; } } );
Anchor to Best practicesBest practices
- Handle draft order states properly: Draft orders can be in different states (open, invoice sent, completed). Always check the draft order status before performing operations, and provide clear feedback when operations aren't applicable to certain states.
- Validate customer information: Many draft order workflows require customer information. Always validate that required customer data (for example, email and address) exists before attempting operations like sending payment links or converting to orders.
- Provide clear conversion workflows: When building workflows that convert draft orders to orders, provide clear confirmation steps and explain what will happen. Draft order conversion is a significant action that merchants need to understand.
- Respect payment status: Be mindful of existing payment requests and payment status. Avoid sending duplicate payment requests or conflicting payment workflows that could confuse customers.
Anchor to LimitationsLimitations
- Single target per module: Each
[[extensions.targeting]]entry in your TOML configuration maps one target to one module file. - Data retention: Draft orders created on or after April 1, 2025 are automatically purged after one year of inactivity.
- 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.