From c686777c7c82de0136ae81d7851a5503d94a3154 Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 20 Nov 2025 00:00:06 +0700 Subject: [PATCH] feat: Stock infinity symbol, sale price display, rich text editor, inline create categories/tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed 4 major UX issues: 1. Stock Column - Show Infinity Symbol Problem: Stock shows badge even when not managed Solution: - Check manage_stock flag - If true: Show StockBadge with quantity - If false: Show ∞ (infinity symbol) for unlimited Result: Clear visual for unlimited stock 2. Type Column & Price Display Problem: Type column empty, price ignores sale price Solution: - Type: Show badge with product.type (simple, variable, etc.) - Price: Respect sale price hierarchy: 1. price_html (WooCommerce formatted) 2. sale_price (show strikethrough regular + green sale) 3. regular_price (normal display) 4. — (dash for no price) Result: - Type visible with badge styling - Sale prices show with strikethrough - Clear visual hierarchy 3. Rich Text Editor for Description Problem: Description shows raw HTML in textarea Solution: - Created RichTextEditor component with Tiptap - Toolbar: Bold, Italic, H2, Lists, Quote, Undo/Redo - Integrated into GeneralTab Features: - WYSIWYG editing - Keyboard shortcuts - Clean toolbar UI - Saves as HTML Result: Professional rich text editing experience 4. Inline Create Categories & Tags Problem: Cannot create new categories/tags in product form Solution: - Added input + "Add" button above each list - Press Enter or click Add to create - Auto-selects newly created item - Shows loading state - Toast notifications Result: - No need to leave product form - Seamless workflow - Better UX Files Changed: - index.tsx: Stock ∞, sale price display, type badge - GeneralTab.tsx: RichTextEditor integration - OrganizationTab.tsx: Inline create UI - RichTextEditor.tsx: New reusable component Note: Variation attribute value issue (screenshot 1) needs API data format investigation --- admin-spa/src/components/RichTextEditor.tsx | 118 ++++++++++++++++++ admin-spa/src/routes/Products/index.tsx | 17 ++- .../Products/partials/tabs/GeneralTab.tsx | 16 +-- .../partials/tabs/OrganizationTab.tsx | 116 ++++++++++++++++- 4 files changed, 252 insertions(+), 15 deletions(-) create mode 100644 admin-spa/src/components/RichTextEditor.tsx diff --git a/admin-spa/src/components/RichTextEditor.tsx b/admin-spa/src/components/RichTextEditor.tsx new file mode 100644 index 0000000..08fdf0b --- /dev/null +++ b/admin-spa/src/components/RichTextEditor.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import { Bold, Italic, List, ListOrdered, Heading2, Quote, Undo, Redo } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +type RichTextEditorProps = { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; +}; + +export function RichTextEditor({ value, onChange, placeholder, className }: RichTextEditorProps) { + const editor = useEditor({ + extensions: [StarterKit], + content: value, + onUpdate: ({ editor }) => { + onChange(editor.getHTML()); + }, + editorProps: { + attributes: { + class: 'prose prose-sm max-w-none focus:outline-none min-h-[150px] px-3 py-2', + }, + }, + }); + + if (!editor) { + return null; + } + + return ( +
+ {/* Toolbar */} +
+ + + + + + +
+ + +
+ + {/* Editor */} + +
+ ); +} diff --git a/admin-spa/src/routes/Products/index.tsx b/admin-spa/src/routes/Products/index.tsx index 21d3ffb..994b3ef 100644 --- a/admin-spa/src/routes/Products/index.tsx +++ b/admin-spa/src/routes/Products/index.tsx @@ -381,18 +381,31 @@ export default function Products() { {product.sku || '—'} - + {product.manage_stock ? ( + + ) : ( + + )} {product.price_html ? ( + ) : product.sale_price ? ( + + {formatMoney(product.regular_price)} + {formatMoney(product.sale_price)} + ) : product.regular_price ? ( {formatMoney(product.regular_price)} ) : ( )} - {product.type || '—'} + + + {product.type || 'simple'} + + {__('Edit')} diff --git a/admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx b/admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx index f8ad9ae..48b67df 100644 --- a/admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx +++ b/admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx @@ -9,6 +9,7 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa import { Separator } from '@/components/ui/separator'; import { DollarSign } from 'lucide-react'; import { getStoreCurrency } from '@/lib/currency'; +import { RichTextEditor } from '@/components/RichTextEditor'; type GeneralTabProps = { name: string; @@ -138,14 +139,13 @@ export function GeneralTab({ {/* Description */}
-