From 58d508eb4e7026d250da1b6ad53257690b85e3d3 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sat, 8 Nov 2025 15:38:38 +0700 Subject: [PATCH] feat: Move action buttons to contextual headers for CRUD pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented proper contextual header pattern for all Order CRUD pages. Problem: - New/Edit pages had action buttons at bottom of form - Detail page showed duplicate headers (contextual + inline) - Not following mobile-first best practices Solution: [Back] Page Title [Action] 1. Edit Order Page Header: [Back] Edit Order #337 [Save] Implementation: - Added formRef to trigger form submit from header - Save button in contextual header - Removed submit button from form bottom - Button shows loading state during save Changes: - Edit.tsx: Added formRef, updated header with Save button - OrderForm.tsx: Added formRef and hideSubmitButton props - Form submit triggered via formRef.current.requestSubmit() 2. New Order Page Header: [Back] New Order [Create] Implementation: - Added formRef to trigger form submit from header - Create button in contextual header - Removed submit button from form bottom - Button shows loading state during creation Changes: - New.tsx: Added formRef, updated header with Create button - Same OrderForm props as Edit page 3. Order Detail Page Header: (hidden) Implementation: - Cleared contextual header completely - Detail page has its own inline header with actions - Inline header: [Back] Order #337 [Print] [Invoice] [Label] [Edit] Changes: - Detail.tsx: clearPageHeader() in useEffect - No duplicate headers OrderForm Component Updates: - Added formRef prop (React.RefObject) - Added hideSubmitButton prop (boolean) - Form element accepts ref:
- Submit button conditionally rendered: {!hideSubmitButton && } - Backward compatible (both props optional) Benefits: ✅ Consistent header pattern across all CRUD pages ✅ Action buttons always visible (sticky header) ✅ Better mobile UX (no scrolling to find buttons) ✅ Loading states in header buttons ✅ Clean, modern interface ✅ Follows industry standards (Gmail, Notion, Linear) Files Modified: - routes/Orders/New.tsx - routes/Orders/Edit.tsx - routes/Orders/Detail.tsx - routes/Orders/partials/OrderForm.tsx Result: ✅ New/Edit: Action buttons in contextual header ✅ Detail: No contextual header (has inline header) ✅ Professional, mobile-first UX! 🎯 --- admin-spa/src/routes/Orders/Detail.tsx | 30 ++----------------- admin-spa/src/routes/Orders/Edit.tsx | 28 ++++++++++++----- admin-spa/src/routes/Orders/New.tsx | 28 ++++++++++++----- .../src/routes/Orders/partials/OrderForm.tsx | 14 ++++++--- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/admin-spa/src/routes/Orders/Detail.tsx b/admin-spa/src/routes/Orders/Detail.tsx index 0fffeae..da31409 100644 --- a/admin-spa/src/routes/Orders/Detail.tsx +++ b/admin-spa/src/routes/Orders/Detail.tsx @@ -142,35 +142,11 @@ export default function OrderShow() { retryPaymentMutation.mutate(); } - // Set page header with actions + // Hide contextual header on detail page (has its own inline header) useEffect(() => { - if (!order || isPrintMode) { - clearPageHeader(); - return; - } - - const actions = ( -
- - - - -
- ); - - setPageHeader( - order.number ? `${__('Order')} #${order.number}` : __('Order'), - actions - ); - + clearPageHeader(); return () => clearPageHeader(); - }, [order, isPrintMode, id, setPageHeader, clearPageHeader]); + }, [clearPageHeader]); useEffect(() => { if (!isPrintMode || !qrRef.current || !order) return; diff --git a/admin-spa/src/routes/Orders/Edit.tsx b/admin-spa/src/routes/Orders/Edit.tsx index 68f9677..6c1fa1b 100644 --- a/admin-spa/src/routes/Orders/Edit.tsx +++ b/admin-spa/src/routes/Orders/Edit.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import OrderForm from '@/routes/Orders/partials/OrderForm'; @@ -17,6 +17,7 @@ export default function OrdersEdit() { const nav = useNavigate(); const qc = useQueryClient(); const { setPageHeader, clearPageHeader } = usePageHeader(); + const formRef = useRef(null); // Hide FAB on edit page useFABConfig('none'); @@ -62,19 +63,28 @@ export default function OrdersEdit() { const order = orderQ.data || {}; - // Set page header with back button + // Set page header with back button and save button useEffect(() => { - const backButton = ( - + const actions = ( +
+ + +
); const title = order.number ? sprintf(__('Edit Order #%s'), order.number) : __('Edit Order'); - setPageHeader(title, backButton); + setPageHeader(title, actions); return () => clearPageHeader(); - }, [order.number, orderId, setPageHeader, clearPageHeader, nav]); + }, [order.number, orderId, upd.isPending, setPageHeader, clearPageHeader, nav]); return (
@@ -91,6 +101,8 @@ export default function OrdersEdit() { shippings={(shippingsQ.data || [])} itemsEditable={['pending', 'on-hold', 'failed', 'draft'].includes(order.status)} showCoupons + formRef={formRef} + hideSubmitButton={true} onSubmit={(form) => { const payload = { ...form } as any; upd.mutate(payload); diff --git a/admin-spa/src/routes/Orders/New.tsx b/admin-spa/src/routes/Orders/New.tsx index b1652e0..92f8bcd 100644 --- a/admin-spa/src/routes/Orders/New.tsx +++ b/admin-spa/src/routes/Orders/New.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { OrdersApi } from '@/lib/api'; import { useNavigate } from 'react-router-dom'; @@ -14,6 +14,7 @@ export default function OrdersNew() { const nav = useNavigate(); const qc = useQueryClient(); const { setPageHeader, clearPageHeader } = usePageHeader(); + const formRef = useRef(null); // Hide FAB on new order page useFABConfig('none'); @@ -44,16 +45,25 @@ export default function OrdersNew() { // Prefer global store currency injected by PHP const { currency: storeCurrency, symbol: storeSymbol } = getStoreCurrency(); - // Set page header with back button + // Set page header with back button and create button useEffect(() => { - const backButton = ( - + const actions = ( +
+ + +
); - setPageHeader(__('New Order'), backButton); + setPageHeader(__('New Order'), actions); return () => clearPageHeader(); - }, [setPageHeader, clearPageHeader, nav]); + }, [mutate.isPending, setPageHeader, clearPageHeader, nav]); return (
@@ -67,6 +77,8 @@ export default function OrdersNew() { defaultCountry={countriesQ.data?.default_country} payments={(payments.data || [])} shippings={(shippings.data || [])} + formRef={formRef} + hideSubmitButton={true} onSubmit={(form) => { mutate.mutate(form as any); }} diff --git a/admin-spa/src/routes/Orders/partials/OrderForm.tsx b/admin-spa/src/routes/Orders/partials/OrderForm.tsx index c7b6f58..e45bae9 100644 --- a/admin-spa/src/routes/Orders/partials/OrderForm.tsx +++ b/admin-spa/src/routes/Orders/partials/OrderForm.tsx @@ -92,6 +92,8 @@ type Props = { rightTop?: React.ReactNode; itemsEditable?: boolean; showCoupons?: boolean; + formRef?: React.RefObject; + hideSubmitButton?: boolean; }; const STATUS_LIST = ['pending','processing','on-hold','completed','cancelled','refunded','failed']; @@ -113,6 +115,8 @@ export default function OrderForm({ showCoupons = true, currency, currencySymbol, + formRef, + hideSubmitButton = false, }: Props) { const oneCountryOnly = countries.length === 1; const firstCountry = countries[0]?.code || 'US'; @@ -346,7 +350,7 @@ export default function OrderForm({ } return ( - + {/* Left: Order details */}
{/* Items and Coupons */} @@ -933,9 +937,11 @@ export default function OrderForm({
)} - + {!hideSubmitButton && ( + + )}