diff --git a/admin-spa/src/routes/Products/SoftwareVersions/index.tsx b/admin-spa/src/routes/Products/SoftwareVersions/index.tsx index 072e8f6..fb498f4 100644 --- a/admin-spa/src/routes/Products/SoftwareVersions/index.tsx +++ b/admin-spa/src/routes/Products/SoftwareVersions/index.tsx @@ -30,7 +30,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; -import { Search, Package, Plus, History, Download, Loader2 } from 'lucide-react'; +import { Search, Package, Plus, History, Download, Loader2, ChevronDown, ChevronRight, X, Pencil } from 'lucide-react'; import { toast } from 'sonner'; import { __ } from '@/lib/i18n'; @@ -43,11 +43,21 @@ interface SoftwareProduct { total_downloads: number; } +interface ChangelogPoint { + type: string; + text: string; +} + +interface ChangelogData { + narrative: string; + points: ChangelogPoint[]; +} + interface SoftwareVersion { id: number; product_id: number; version: string; - changelog: string; + changelog: ChangelogData | string; release_date: string; is_current: boolean; download_count: number; @@ -72,7 +82,13 @@ export default function SoftwareVersions() { const [search, setSearch] = useState(''); const [selectedProduct, setSelectedProduct] = useState(null); const [isAddVersionOpen, setIsAddVersionOpen] = useState(false); - const [newVersion, setNewVersion] = useState({ version: '', changelog: '' }); + const [editingVersionId, setEditingVersionId] = useState(null); + const [newVersion, setNewVersion] = useState({ + version: '', + changelog: { narrative: '', points: [] as ChangelogPoint[] } + }); + const [expandedVersions, setExpandedVersions] = useState>({}); + const queryClient = useQueryClient(); // Fetch software-enabled products @@ -80,15 +96,14 @@ export default function SoftwareVersions() { queryKey: ['software-products'], queryFn: async () => { const response = await api.get('/products?software_enabled=true&per_page=100'); - // Filter products that have software distribution enabled - const products = (response as any).products || []; + const products = (response as any).rows || []; return { - products: products.filter((p: any) => p.meta?._woonoow_software_enabled === 'yes').map((p: any) => ({ + products: products.map((p: any) => ({ id: p.id, name: p.name, - slug: p.meta?._woonoow_software_slug || '', - current_version: p.meta?._woonoow_software_current_version || '', - wp_enabled: p.meta?._woonoow_software_wp_enabled === 'yes', + slug: p.software_slug || p.meta?._woonoow_software_slug || '', + current_version: p.software_current_version || p.meta?._woonoow_software_current_version || '', + wp_enabled: p.software_wp_enabled || p.meta?._woonoow_software_wp_enabled === 'yes', total_downloads: 0, })) } as ProductsResponse; @@ -107,21 +122,121 @@ export default function SoftwareVersions() { // Add new version mutation const addVersion = useMutation({ - mutationFn: async (data: { version: string; changelog: string }) => { + mutationFn: async (data: { version: string; changelog: ChangelogData }) => { return await api.post(`/software/products/${selectedProduct}/versions`, data); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['software-versions', selectedProduct] }); queryClient.invalidateQueries({ queryKey: ['software-products'] }); toast.success(__('Version added successfully')); - setIsAddVersionOpen(false); - setNewVersion({ version: '', changelog: '' }); + closeModal(); }, onError: (error: any) => { toast.error(error.message || __('Failed to add version')); }, }); + // Edit version mutation + const editVersion = useMutation({ + mutationFn: async (data: { version_id: number; version: string; changelog: ChangelogData }) => { + return await api.put(`/software/products/${selectedProduct}/versions/${data.version_id}`, { + version: data.version, + changelog: data.changelog + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['software-versions', selectedProduct] }); + queryClient.invalidateQueries({ queryKey: ['software-products'] }); + toast.success(__('Version updated successfully')); + closeModal(); + }, + onError: (error: any) => { + toast.error(error.message || __('Failed to update version')); + }, + }); + + const openAddModal = () => { + setEditingVersionId(null); + setNewVersion({ version: '', changelog: { narrative: '', points: [] } }); + setIsAddVersionOpen(true); + }; + + const openEditModal = (version: SoftwareVersion, e: React.MouseEvent) => { + e.stopPropagation(); + const cl = typeof version.changelog === 'object' && version.changelog !== null + ? (version.changelog as ChangelogData) + : { narrative: version.changelog as string, points: [] }; + + setEditingVersionId(version.id); + setNewVersion({ + version: version.version, + changelog: { narrative: cl.narrative || '', points: cl.points || [] } + }); + setIsAddVersionOpen(true); + }; + + const closeModal = () => { + setIsAddVersionOpen(false); + setEditingVersionId(null); + setNewVersion({ version: '', changelog: { narrative: '', points: [] } }); + }; + + const handleSaveVersion = () => { + if (editingVersionId) { + editVersion.mutate({ + version_id: editingVersionId, + ...newVersion + }); + } else { + addVersion.mutate(newVersion); + } + }; + + const toggleVersion = (id: number) => { + setExpandedVersions(prev => ({ + ...prev, + [id]: !prev[id] + })); + }; + + const addChangelogPoint = () => { + setNewVersion(prev => ({ + ...prev, + changelog: { + ...prev.changelog, + points: [...prev.changelog.points, { type: 'ADD', text: '' }] + } + })); + }; + + const updateChangelogPoint = (index: number, field: 'type' | 'text', value: string) => { + setNewVersion(prev => { + const newPoints = [...prev.changelog.points]; + newPoints[index] = { ...newPoints[index], [field]: value }; + return { + ...prev, + changelog: { + ...prev.changelog, + points: newPoints + } + }; + }); + }; + + const removeChangelogPoint = (index: number) => { + setNewVersion(prev => { + const newPoints = [...prev.changelog.points]; + newPoints.splice(index, 1); + return { + ...prev, + changelog: { + ...prev.changelog, + points: newPoints + } + }; + }); + }; + const filteredProducts = productsData?.products?.filter(p => p.name.toLowerCase().includes(search.toLowerCase()) || p.slug.toLowerCase().includes(search.toLowerCase()) @@ -129,9 +244,20 @@ export default function SoftwareVersions() { const selectedProductData = productsData?.products?.find(p => p.id === selectedProduct); + const getBadgeColor = (type: string) => { + switch (type) { + case 'ADD': return 'bg-emerald-500 hover:bg-emerald-600'; + case 'FIX': return 'bg-orange-500 hover:bg-orange-600'; + case 'IMPROVE': return 'bg-blue-500 hover:bg-blue-600'; + case 'DROP': return 'bg-rose-500 hover:bg-rose-600'; + default: return 'bg-gray-500 hover:bg-gray-600'; + } + }; + + const isSaving = addVersion.isPending || editVersion.isPending; + return (
- {/* Header */}

{__('Software Versions')}

@@ -142,7 +268,6 @@ export default function SoftwareVersions() {
- {/* Products List */}

{__('Software Products')}

@@ -176,8 +301,7 @@ export default function SoftwareVersions() {
- {/* Version Details */}
{!selectedProduct ? (
@@ -217,7 +340,6 @@ export default function SoftwareVersions() {
) : ( <> - {/* Version Header */}

{selectedProductData?.name}

@@ -225,21 +347,25 @@ export default function SoftwareVersions() { {__('Current version')}: {versionsData?.config?.current_version || '—'}

- + !open ? closeModal() : setIsAddVersionOpen(true)}> - - + - {__('Add New Version')} + + {editingVersionId ? __('Edit Version') : __('Add New Version')} + - {__('Release a new version of')} {selectedProductData?.name} + {editingVersionId + ? `${__('Modify release details for')} ${selectedProductData?.name}` + : `${__('Release a new version of')} ${selectedProductData?.name}`} -
+
setNewVersion(prev => ({ ...prev, version: e.target.value }))} /> -

- {__('Use semantic versioning (e.g., 1.0.0, 1.2.3)')} -

-
- -