Commit c9e5398403f for woocommerce
commit c9e5398403f430c12ad0595ef5853a20799f8612
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date: Fri Mar 27 16:41:01 2026 +0300
Fulfillment UI/UX minor tweaks (#63782)
* Fulfillment UI/UX minor tweaks
- Make entire card header bar clickable to expand/collapse with keyboard support
- Reduce card side padding from 24px to 16px
- Keep latest fulfillment open when all items are fulfilled
- Make fulfillment status badge clickable to open the drawer
- Show "Copied" confirmation with checkmark icon when copying tracking number
- Resolve shipment provider key to display label in order list column
- Recalculate tracking URL when tracking number changes with a provider selected
* Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin
* Fix TS error: guard fulfillments against undefined in else branch
* Address review feedback across all fulfillment tweaks
- Fix CopyIcon setTimeout leak: use useRef + useEffect cleanup
- Fix provider label resolution: track which fulfillment owns the provider
instead of always using fulfillments[0]
- Add role="button" and tabindex="0" to badge mark element for keyboard access
- Restore deterministic else branch in drawer context for undefined/empty state
- Simplify padding shorthand (16px 16px -> 16px)
- Fix card test: use initialState="expanded" (matching the actual prop type)
- Strengthen PHP test assertion to specifically match badge mark element
* Handle clipboard write failure and adjust badge wrapper layout
- Await clipboard.writeText promise; only show "Copied" on success,
log error on failure
- Change fulfillment-status-wrapper to justify-content: flex-start
* Make tracking URL navigable in shipment viewer
Add optional href prop to MetaList items. When provided, the value
renders as a link that opens in a new tab. The shipment viewer passes
the tracking URL as href when available.
* Link tracking numbers to tracking URLs in orders list
When a single fulfillment has both a tracking number and URL, render
the tracking number as a link in the Shipment Tracking column with
color #2f2f2f. Meta-list tracking URL link uses default color.
* Add underline to linked tracking url on orders list
* Revert fulfillment-status-wrapper to justify-content: space-between
* Address CodeRabbit review feedback
- Reinitialize mockContext via factory function in beforeEach to avoid
shared state across tests
- Use canonical get_shipping_provider() instead of
get_meta('_shipment_provider') in provider column renderer
- Use composite key for "other" providers with different _provider_name
values so they aren't collapsed into one entry
* Use Fulfillment setters instead of raw meta calls in tests
* Address code review feedback
- Fix inconsistent cursor pointers on fulfillment status wrapper
- Stop event propagation on copy button to prevent card toggle
* Fix fulfillment trigger styling
* Fix FulfillmentCard test failures after trunk merge
The merge introduced a Button component to FulfillmentCard but the test
mock only included Icon, making Button undefined. Added Button to the
mock and updated selectors to disambiguate between the header div
(role="button") and the collapse/expand button element.
* Fix lint issues in FulfillmentCard test
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/63782-fix-fulfillment-minor-tweaks b/plugins/woocommerce/changelog/63782-fix-fulfillment-minor-tweaks
new file mode 100644
index 00000000000..f6ee6426561
--- /dev/null
+++ b/plugins/woocommerce/changelog/63782-fix-fulfillment-minor-tweaks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Fulfillment UI/UX minor tweaks: clickable card headers, reduced padding, copy confirmation, provider label resolution, and tracking URL auto-update.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
index bc4ae6f81a5..4502130f7af 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
@@ -70,6 +70,21 @@ export default function ShipmentManualEntryForm() {
value={ trackingNumber }
onChange={ ( value: string ) => {
setTrackingNumber( value );
+ if (
+ shipmentProvider &&
+ shipmentProvider !== 'other'
+ ) {
+ setTrackingUrl(
+ (
+ window.wcFulfillmentSettings.providers[
+ shipmentProvider
+ ]?.url ?? ''
+ ).replace(
+ /__placeholder__/i,
+ encodeURIComponent( value )
+ )
+ );
+ }
} }
__next40pxDefaultSize
__nextHasNoMarginBottom
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-viewer.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-viewer.tsx
index cb3e935f508..c9cdd34819b 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-viewer.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-viewer.tsx
@@ -92,6 +92,7 @@ export default function ShipmentViewer() {
{
label: __( 'Tracking URL', 'woocommerce' ),
value: trackingUrl,
+ href: trackingUrl || undefined,
},
] }
/>
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-manual-entry-form.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-manual-entry-form.test.js
index fbaed33d78e..0c1d6a6e5ba 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-manual-entry-form.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-manual-entry-form.test.js
@@ -37,20 +37,22 @@ jest.mock( '@wordpress/components', () => ( {
),
} ) );
+const createMockContext = () => ( {
+ trackingNumber: '',
+ setTrackingNumber: jest.fn(),
+ shipmentProvider: '',
+ setShipmentProvider: jest.fn(),
+ providerName: '',
+ setProviderName: jest.fn(),
+ trackingUrl: '',
+ setTrackingUrl: jest.fn(),
+} );
+
describe( 'ShipmentManualEntryForm', () => {
- const mockContext = {
- trackingNumber: '',
- setTrackingNumber: jest.fn(),
- shipmentProvider: '',
- setShipmentProvider: jest.fn(),
- providerName: '',
- setProviderName: jest.fn(),
- trackingUrl: '',
- setTrackingUrl: jest.fn(),
- };
+ let mockContext;
beforeEach( () => {
- jest.clearAllMocks();
+ mockContext = createMockContext();
useShipmentFormContext.mockReturnValue( mockContext );
} );
@@ -141,4 +143,30 @@ describe( 'ShipmentManualEntryForm', () => {
fireEvent.change( combobox, { target: { value: 'unknown-provider' } } );
expect( mockContext.setTrackingUrl ).toHaveBeenCalledWith( '' );
} );
+
+ it( 'updates tracking URL when tracking number changes and provider is selected', () => {
+ mockContext.shipmentProvider = 'ups';
+ render( <ShipmentManualEntryForm /> );
+ const input = screen.getByPlaceholderText( 'Enter tracking number' );
+ fireEvent.change( input, { target: { value: 'NEWTRACK123' } } );
+ expect( mockContext.setTrackingUrl ).toHaveBeenCalledWith(
+ 'https://www.ups.com/track?loc=en_US&tracknum=NEWTRACK123'
+ );
+ } );
+
+ it( 'does not update tracking URL when tracking number changes and provider is "other"', () => {
+ mockContext.shipmentProvider = 'other';
+ render( <ShipmentManualEntryForm /> );
+ const input = screen.getByPlaceholderText( 'Enter tracking number' );
+ fireEvent.change( input, { target: { value: 'NEWTRACK123' } } );
+ expect( mockContext.setTrackingUrl ).not.toHaveBeenCalled();
+ } );
+
+ it( 'does not update tracking URL when tracking number changes and no provider is selected', () => {
+ mockContext.shipmentProvider = '';
+ render( <ShipmentManualEntryForm /> );
+ const input = screen.getByPlaceholderText( 'Enter tracking number' );
+ fireEvent.change( input, { target: { value: 'NEWTRACK123' } } );
+ expect( mockContext.setTrackingUrl ).not.toHaveBeenCalled();
+ } );
} );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.scss b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.scss
index 71c06d1924b..4c726abbfc9 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.scss
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.scss
@@ -5,14 +5,14 @@
flex-direction: column;
& &__header {
- & &--clickable {
+ &--clickable {
cursor: pointer;
}
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
- padding: 16px 24px;
+ padding: 16px;
h3 {
display: flex;
@@ -34,36 +34,22 @@
box-sizing: border-box;
padding: 2px 4px;
}
- button {
- padding: 0;
- width: 24px;
- height: 24px;
- border: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-
- &:focus {
- outline: none;
- box-shadow: none;
- }
- }
}
& &__body {
display: flex;
- padding: 8px 24px 24px 24px;
+ padding: 8px 16px 16px 16px;
&.no-collapse {
- padding: 0 24px 24px 24px;
+ padding: 0 16px 16px 16px;
}
}
}
.woocommerce-fulfillment-card__size-small {
.woocommerce-fulfillment-card__header {
- padding: 16px 24px 0;
+ padding: 16px 16px 0;
}
.woocommerce-fulfillment-card__body {
- padding: 8px 24px 16px 24px !important;
+ padding: 8px 16px 16px 16px !important;
}
}
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.tsx
index 009acb9f5f5..6cbcfa12ac3 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.tsx
@@ -26,11 +26,34 @@ export default function FulfillmentCard( {
const [ isOpen, setIsOpen ] = useState( initialState === 'expanded' );
const hasChildren = React.Children.toArray( children ).length > 0;
+ const handleToggle = () => setIsOpen( ! isOpen );
+ const handleKeyUp = ( e: React.KeyboardEvent ) => {
+ if ( e.key === 'Enter' || e.key === ' ' ) {
+ e.preventDefault();
+ handleToggle();
+ }
+ };
+
return (
<div
className={ `woocommerce-fulfillment-card woocommerce-fulfillment-card__size-${ size }` }
>
- <div className="woocommerce-fulfillment-card__header">
+ <div
+ className={ [
+ 'woocommerce-fulfillment-card__header',
+ isCollapsable
+ ? 'woocommerce-fulfillment-card__header--clickable'
+ : '',
+ ].join( ' ' ) }
+ { ...( isCollapsable
+ ? {
+ onClick: handleToggle,
+ onKeyUp: handleKeyUp,
+ role: 'button',
+ tabIndex: 0,
+ }
+ : {} ) }
+ >
{ header }
{ isCollapsable && (
<Button
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/test/card.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/test/card.test.js
index 197985a705e..fc6697b6886 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/test/card.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/test/card.test.js
@@ -9,10 +9,8 @@ import { render, screen, fireEvent } from '@testing-library/react';
import FulfillmentCard from '../card';
jest.mock( '@wordpress/components', () => ( {
- Button: ( { onClick, children } ) => (
- <button data-testid="button" onClick={ onClick }>
- { children }
- </button>
+ Button: ( { children, ...props } ) => (
+ <button { ...props }>{ children }</button>
),
Icon: ( { icon } ) => <span data-testid="icon">{ icon }</span>,
} ) );
@@ -28,8 +26,10 @@ describe( 'FulfillmentCard', () => {
expect( screen.getByText( 'Header' ) ).toBeInTheDocument();
// Children should not be visible by default for collapsable
expect( screen.queryByText( 'Child content' ) ).not.toBeInTheDocument();
- // Click to expand
- fireEvent.click( screen.getByTestId( 'button' ) );
+ // Click the header div (role="button") to expand
+ fireEvent.click(
+ screen.getByText( 'Header' ).closest( '[role="button"]' )
+ );
expect( screen.getByText( 'Child content' ) ).toBeInTheDocument();
} );
@@ -40,42 +40,42 @@ describe( 'FulfillmentCard', () => {
</FulfillmentCard>
);
- const button = screen.getByTestId( 'button' );
+ const header = screen
+ .getByText( 'Header' )
+ .closest( '[role="button"]' );
expect( screen.queryByText( 'Child content' ) ).not.toBeInTheDocument();
- fireEvent.click( button );
+ fireEvent.click( header );
expect( screen.getByText( 'Child content' ) ).toBeInTheDocument();
- fireEvent.click( button );
+ fireEvent.click( header );
expect( screen.queryByText( 'Child content' ) ).not.toBeInTheDocument();
} );
- it( 'renders without collapse button when not collapsable', () => {
+ it( 'renders without clickable header when not collapsable', () => {
render(
<FulfillmentCard header={ <h1>Header</h1> } isCollapsable={ false }>
<p>Child content</p>
</FulfillmentCard>
);
- expect( screen.queryByTestId( 'button' ) ).not.toBeInTheDocument();
+ expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument();
// Children should not be visible if not collapsable (matches component behavior)
expect( screen.queryByText( 'Child content' ) ).not.toBeInTheDocument();
} );
- it( 'renders children if initialState is open (collapsable)', () => {
+ it( 'renders children if initialState is expanded (collapsable)', () => {
render(
<FulfillmentCard
header={ <h1>Header</h1> }
isCollapsable
- initialState="open"
+ initialState="expanded"
>
<p>Child content</p>
</FulfillmentCard>
);
- const button = screen.getByTestId( 'button' );
- // Children may not be visible by default, so click to expand
- fireEvent.click( button );
+ // Children should be visible immediately when initialState is expanded
expect( screen.getByText( 'Child content' ) ).toBeInTheDocument();
} );
@@ -90,7 +90,28 @@ describe( 'FulfillmentCard', () => {
</FulfillmentCard>
);
- expect( screen.getByTestId( 'button' ) ).toBeInTheDocument();
+ expect(
+ screen.getByText( 'Header' ).closest( '[role="button"]' )
+ ).toBeInTheDocument();
+ expect( screen.queryByText( 'Child content' ) ).not.toBeInTheDocument();
+ } );
+
+ it( 'supports keyboard interaction on collapsable header', () => {
+ render(
+ <FulfillmentCard header={ <h1>Header</h1> } isCollapsable>
+ <p>Child content</p>
+ </FulfillmentCard>
+ );
+
+ const header = screen
+ .getByText( 'Header' )
+ .closest( '[role="button"]' );
+ expect( screen.queryByText( 'Child content' ) ).not.toBeInTheDocument();
+
+ fireEvent.keyUp( header, { key: 'Enter' } );
+ expect( screen.getByText( 'Child content' ) ).toBeInTheDocument();
+
+ fireEvent.keyUp( header, { key: ' ' } );
expect( screen.queryByText( 'Child content' ) ).not.toBeInTheDocument();
} );
} );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/meta-list/meta-list.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/meta-list/meta-list.tsx
index 207eb76d037..58f020a4bce 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/meta-list/meta-list.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/meta-list/meta-list.tsx
@@ -9,12 +9,29 @@ import { isEmpty } from 'lodash';
*/
import './meta-list.scss';
+function MetaValue( { value, href }: { value: string; href?: string } ) {
+ if ( isEmpty( String( value ) ) ) {
+ return <>{ __( '(empty)', 'woocommerce' ) }</>;
+ }
+
+ if ( href ) {
+ return (
+ <a href={ href } target="_blank" rel="noopener noreferrer">
+ { String( value ) }
+ </a>
+ );
+ }
+
+ return <>{ String( value ) }</>;
+}
+
export default function MetaList( {
metaList,
}: {
metaList: Array< {
label: string;
value: string;
+ href?: string;
} >;
} ) {
return (
@@ -28,9 +45,7 @@ export default function MetaList( {
{ meta.label }
</div>
<div className="woocommerce-fulfillment-meta-list__item-value">
- { isEmpty( String( meta.value ) )
- ? __( '(empty)', 'woocommerce' )
- : String( meta.value ) }
+ <MetaValue value={ meta.value } href={ meta.href } />
</div>
</li>
) ) }
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/context/drawer-context.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/context/drawer-context.tsx
index 1a87d12bcf8..33184f04b32 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/context/drawer-context.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/context/drawer-context.tsx
@@ -104,9 +104,14 @@ export const FulfillmentDrawerProvider = ( {
// If all the items are in a single fulfillment,
// open that fulfillment section directly.
setOpenSection( 'fulfillment-' + fulfillments[ 0 ].id );
- } else {
+ } else if ( fulfillments && fulfillments.length > 0 ) {
// If there are no pending items and multiple fulfillments,
- // collapse all.
+ // open the latest fulfillment.
+ setOpenSection(
+ 'fulfillment-' + fulfillments[ fulfillments.length - 1 ].id
+ );
+ } else {
+ // No fulfillments data yet or empty, collapse all.
setOpenSection( '' );
}
}, [ orderId, fulfillments, order, refunds ] );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/style.scss b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/style.scss
index c93df585dbd..bfb3ebbc272 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/style.scss
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/style.scss
@@ -457,6 +457,7 @@ tr.type-shop_order.is-selected {
gap: 12px;
.fulfillments-trigger {
margin-left: -8px;
+ cursor: pointer !important;
svg {
fill: #949494;
}
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/utils/icons.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/utils/icons.tsx
index ad9043bbdd3..04018c138b3 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/utils/icons.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/utils/icons.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import { Button } from '@wordpress/components';
+import { useEffect, useRef, useState } from 'react';
export const SearchIcon = () => (
<svg
@@ -68,30 +69,75 @@ export const EnvelopeIcon = () => (
);
export const CopyIcon = ( { copyText }: { copyText: string } ) => {
+ const [ copied, setCopied ] = useState( false );
+ const timeoutRef = useRef< ReturnType< typeof setTimeout > >();
+
+ useEffect( () => {
+ return () => {
+ if ( timeoutRef.current ) {
+ clearTimeout( timeoutRef.current );
+ }
+ };
+ }, [] );
+
+ const handleCopy = ( event: React.MouseEvent ) => {
+ event.stopPropagation();
+ navigator.clipboard.writeText( copyText ).then(
+ () => {
+ setCopied( true );
+ if ( timeoutRef.current ) {
+ clearTimeout( timeoutRef.current );
+ }
+ timeoutRef.current = setTimeout(
+ () => setCopied( false ),
+ 2000
+ );
+ },
+ ( err ) => {
+ // eslint-disable-next-line no-console
+ console.error( 'Failed to copy to clipboard:', err );
+ }
+ );
+ };
+
return (
<Button
size="small"
iconSize={ 14 }
- onClick={ () => {
- navigator.clipboard.writeText( copyText );
- } }
+ onClick={ handleCopy }
icon={
- <svg
- width="14"
- height="14"
- viewBox="0 0 14 14"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path
- fillRule="evenodd"
- clipRule="evenodd"
- d="M1.68815 1.5835H9.81315C9.87065 1.5835 9.91732 1.63016 9.91732 1.68766V9.81266C9.91732 9.84029 9.90634 9.86679 9.88681 9.88632C9.86727 9.90586 9.84078 9.91683 9.81315 9.91683H1.68815C1.66052 9.91683 1.63403 9.90586 1.61449 9.88632C1.59496 9.86679 1.58398 9.84029 1.58398 9.81266V1.68766C1.58398 1.63016 1.63065 1.5835 1.68815 1.5835ZM0.333984 1.68766C0.333984 0.940163 0.940651 0.333496 1.68815 0.333496H9.81315C10.5615 0.333496 11.1673 0.940163 11.1673 1.68766V9.81266C11.1673 10.561 10.5615 11.1668 9.81315 11.1668H1.68815C1.329 11.1668 0.984566 11.0242 0.730611 10.7702C0.476655 10.5162 0.333984 10.1718 0.333984 9.81266V1.68766ZM12.4173 11.401V3.901H13.6673V11.401C13.6673 12.6668 12.6423 13.6668 11.3765 13.6668H2.20898V12.4168H11.3765C11.9515 12.4168 12.4173 11.9768 12.4173 11.401Z"
- fill="#949494"
- />
- </svg>
+ copied ? (
+ <svg
+ width="14"
+ height="14"
+ viewBox="0 0 14 14"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M5.25 10.5L1.75 7L2.6275 6.1225L5.25 8.7375L11.3725 2.625L12.25 3.5L5.25 10.5Z"
+ fill="#008A20"
+ />
+ </svg>
+ ) : (
+ <svg
+ width="14"
+ height="14"
+ viewBox="0 0 14 14"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ fillRule="evenodd"
+ clipRule="evenodd"
+ d="M1.68815 1.5835H9.81315C9.87065 1.5835 9.91732 1.63016 9.91732 1.68766V9.81266C9.91732 9.84029 9.90634 9.86679 9.88681 9.88632C9.86727 9.90586 9.84078 9.91683 9.81315 9.91683H1.68815C1.66052 9.91683 1.63403 9.90586 1.61449 9.88632C1.59496 9.86679 1.58398 9.84029 1.58398 9.81266V1.68766C1.58398 1.63016 1.63065 1.5835 1.68815 1.5835ZM0.333984 1.68766C0.333984 0.940163 0.940651 0.333496 1.68815 0.333496H9.81315C10.5615 0.333496 11.1673 0.940163 11.1673 1.68766V9.81266C11.1673 10.561 10.5615 11.1668 9.81315 11.1668H1.68815C1.329 11.1668 0.984566 11.0242 0.730611 10.7702C0.476655 10.5162 0.333984 10.1718 0.333984 9.81266V1.68766ZM12.4173 11.401V3.901H13.6673V11.401C13.6673 12.6668 12.6423 13.6668 11.3765 13.6668H2.20898V12.4168H11.3765C11.9515 12.4168 12.4173 11.9768 12.4173 11.401Z"
+ fill="#949494"
+ />
+ </svg>
+ )
}
- label={ 'Copy' }
+ aria-label={ copied ? 'Copied' : 'Copy' }
+ label={ copied ? 'Copied' : 'Copy' }
__next40pxDefaultSize
/>
);
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
index 50e71e4902e..f6511f73a48 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
@@ -162,7 +162,7 @@ class FulfillmentsRenderer {
);
}
- echo '<mark class="fulfillment-status" style="background-color:' . esc_attr( $status_props['background_color'] ) . '; color: ' . esc_attr( $status_props['text_color'] ) . '"><span>' . esc_html( $status_props['label'] ) . '</span></mark>';
+ echo '<mark class="fulfillment-status fulfillments-trigger" style="background-color:' . esc_attr( $status_props['background_color'] ) . '; color: ' . esc_attr( $status_props['text_color'] ) . ';" role="button" tabindex="0" data-order-id="' . esc_attr( (string) $order->get_id() ) . '"><span>' . esc_html( $status_props['label'] ) . '</span></mark>';
echo "<a href='#' class='fulfillments-trigger' data-order-id='" . esc_attr( $order->get_id() ) . "' title='" . esc_attr__( 'View Fulfillments', 'woocommerce' ) . "'>
<svg width='16' height='16' viewBox='0 0 12 14' xmlns='http://www.w3.org/2000/svg'>
<path d='M11.8333 2.83301L9.33329 0.333008L2.24996 7.41634L1.41663 10.7497L4.74996 9.91634L11.8333 2.83301ZM5.99996 12.4163H0.166626V13.6663H5.99996V12.4163Z' />
@@ -179,20 +179,26 @@ class FulfillmentsRenderer {
private function render_shipment_provider_column_row_data( WC_Order $order, array $fulfillments ) {
$providers = array();
foreach ( $fulfillments as $fulfillment ) {
- $providers[] = $fulfillment->get_shipment_provider();
- }
-
- $providers = array_filter(
- $providers,
- function ( $provider ) {
- return ! empty( $provider );
+ $provider = $fulfillment->get_shipment_provider();
+ if ( ! empty( $provider ) ) {
+ $provider_name = $fulfillment->get_meta( '_provider_name' );
+ $key = 'other' === $provider && ! empty( $provider_name )
+ ? $provider . '::' . $provider_name
+ : $provider;
+ $providers[ $key ] = $fulfillment;
}
- );
+ }
if ( count( $providers ) > 1 ) {
echo '<span>' . esc_html__( 'Multiple providers', 'woocommerce' ) . '</span>';
} elseif ( 1 === count( $providers ) ) {
- echo '<span>' . esc_html( array_shift( $providers ) ) . '</span>';
+ $provider_fulfillment = reset( $providers );
+ $provider_slug = $provider_fulfillment->get_shipment_provider();
+ $known_providers = FulfillmentUtils::get_shipping_providers_object();
+ $provider_name_meta = $provider_fulfillment->get_meta( '_provider_name' );
+ $provider_display_label = $known_providers[ $provider_slug ]['label']
+ ?? ( ! empty( $provider_name_meta ) ? $provider_name_meta : $provider_slug );
+ echo '<span>' . esc_html( $provider_display_label ) . '</span>';
} else {
echo '<span>--</span>';
}
@@ -207,20 +213,24 @@ class FulfillmentsRenderer {
private function render_shipment_tracking_column_row_data( WC_Order $order, array $fulfillments ) {
$tracking = array();
foreach ( $fulfillments as $fulfillment ) {
- $tracking[] = $fulfillment->get_tracking_number();
- }
-
- $tracking = array_filter(
- $tracking,
- function ( $provider ) {
- return ! empty( $provider );
+ $number = $fulfillment->get_tracking_number();
+ if ( ! empty( $number ) ) {
+ $tracking[] = array(
+ 'number' => $number,
+ 'url' => $fulfillment->get_tracking_url(),
+ );
}
- );
+ }
if ( count( $tracking ) > 1 ) {
echo '<span>' . esc_html__( 'Multiple trackings', 'woocommerce' ) . '</span>';
} elseif ( 1 === count( $tracking ) ) {
- echo '<span>' . esc_html( array_shift( $tracking ) ) . '</span>';
+ $entry = $tracking[0];
+ if ( ! empty( $entry['url'] ) ) {
+ echo '<a href="' . esc_url( $entry['url'] ) . '" target="_blank" rel="noopener noreferrer" style="text-decoration: underline; color: #2f2f2f;">' . esc_html( $entry['number'] ) . '</a>';
+ } else {
+ echo '<span>' . esc_html( $entry['number'] ) . '</span>';
+ }
} else {
echo '<span>--</span>';
}
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentOrderNotesTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentOrderNotesTest.php
index 0dbccb46ce3..fb6d6a7673b 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentOrderNotesTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentOrderNotesTest.php
@@ -304,9 +304,9 @@ class FulfillmentOrderNotesTest extends \WC_Unit_Test_Case {
);
// Update with tracking info (non-status change).
- $fulfillment->update_meta_data( '_tracking_number', 'UPS999' );
- $fulfillment->update_meta_data( '_shipment_provider', 'ups' );
- $fulfillment->update_meta_data( '_tracking_url', 'https://ups.com/track/UPS999' );
+ $fulfillment->set_tracking_number( 'UPS999' );
+ $fulfillment->set_shipment_provider( 'ups' );
+ $fulfillment->set_tracking_url( 'https://ups.com/track/UPS999' );
$fulfillment->save();
$notes = wc_get_order_notes( array( 'order_id' => $order->get_id() ) );
@@ -399,7 +399,7 @@ class FulfillmentOrderNotesTest extends \WC_Unit_Test_Case {
);
// Update tracking number (non-status change).
- $fulfillment->update_meta_data( '_tracking_number', 'NEWTRACK789' );
+ $fulfillment->set_tracking_number( 'NEWTRACK789' );
$fulfillment->save();
$notes = wc_get_order_notes( array( 'order_id' => $order->get_id() ) );
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsRendererTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsRendererTest.php
index 572680d41b5..a790e11cca4 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsRendererTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsRendererTest.php
@@ -117,6 +117,8 @@ class FulfillmentsRendererTest extends \WC_Unit_Test_Case {
$this->assertStringContainsString( 'Fulfilled', $output );
$this->assertStringContainsString( '123456789', $output );
$this->assertStringContainsString( 'UPS', $output );
+ $this->assertStringContainsString( '<mark class="fulfillment-status fulfillments-trigger"', $output );
+ $this->assertStringContainsString( 'data-order-id="' . $order->get_id() . '"', $output );
$this->assertStringContainsString( "<a href='#' class='fulfillments-trigger' data-order-id='" . $order->get_id() . "' title='" . esc_attr__( 'View Fulfillments', 'woocommerce' ) . "'>", $output );
$this->assertStringContainsString( '<svg ', $output );
$this->assertStringContainsString( '<path ', $output );