feat: rewrite DataTable + design system with shadcn/ui, replace SweetAlert2
- Rewrite DataTable using shadcn Table, Button, Dialog, Input, Select, Checkbox, Skeleton - Replace all SweetAlert2 calls with shadcn Dialog (confirm) + sonner toast - Rewrite design system internals to use shadcn Label, Input, Switch, Button, Alert, Badge, Tabs - Add Toaster to App.js for global toast support - Remove all @wordpress/components imports from DataTable - Remove WpcftoDesign.css import from design system - Replace Swal.fire() in Coupons, Products, Forms, Access, Licenses pages
This commit is contained in:
1111
build/admin-rtl.css
1111
build/admin-rtl.css
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash', 'wp-primitives'), 'version' => '9a1a0e2d03b8d775e648');
|
<?php return array('dependencies' => array('react', 'react-dom', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash', 'wp-primitives'), 'version' => '6bf4643f2ea15ecdf0b7');
|
||||||
|
|||||||
1111
build/admin.css
1111
build/admin.css
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@ import CouponsPage from '../pages/Coupons';
|
|||||||
import AccessPage from '../pages/Access';
|
import AccessPage from '../pages/Access';
|
||||||
import LicensesPage from '../pages/Licenses';
|
import LicensesPage from '../pages/Licenses';
|
||||||
import NavigationMenu from './NavigationMenu';
|
import NavigationMenu from './NavigationMenu';
|
||||||
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import '../pages/AdminPages.css';
|
import '../pages/AdminPages.css';
|
||||||
|
|
||||||
const pageComponents = {
|
const pageComponents = {
|
||||||
@@ -101,6 +102,7 @@ export default function App({ page: initialPage, initialData }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="formipay-admin-wrap">
|
<div className="formipay-admin-wrap">
|
||||||
|
<Toaster />
|
||||||
<NavigationMenu
|
<NavigationMenu
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
onPageNavigate={handlePageNavigate}
|
onPageNavigate={handlePageNavigate}
|
||||||
|
|||||||
@@ -1,500 +1,555 @@
|
|||||||
/**
|
/**
|
||||||
* Full-featured DataTable component
|
* Full-featured DataTable component
|
||||||
* Supports: selection, filtering, search, sort, pagination, actions
|
* Supports: selection, filtering, search, sort, pagination, actions
|
||||||
|
*
|
||||||
|
* Built with shadcn/ui components + Tailwind CSS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||||
import {
|
import { cn } from '@/lib/utils';
|
||||||
Button,
|
import { confirm } from '@/lib/confirm';
|
||||||
Modal,
|
import { toast } from '@/lib/toast';
|
||||||
TextControl,
|
|
||||||
SelectControl,
|
|
||||||
Spinner,
|
|
||||||
} from '@wordpress/components';
|
|
||||||
import './DataTable.css';
|
|
||||||
|
|
||||||
// SweetAlert2 is loaded via WordPress (global scope)
|
// shadcn/ui components
|
||||||
const Swal = window.Swal;
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableCell,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|
||||||
export default function DataTable({
|
export default function DataTable({
|
||||||
// Data fetching
|
// Data fetching
|
||||||
initialData = [],
|
initialData = [],
|
||||||
|
|
||||||
// Columns definition
|
// Columns definition
|
||||||
columns,
|
columns,
|
||||||
|
|
||||||
// Filtering
|
// Filtering
|
||||||
filterOptions = null, // { key: 'post_status', options: [{value, label}] }
|
filterOptions = null, // { key: 'post_status', options: [{value, label}] }
|
||||||
statusCounts = null, // { all: 10, publish: 5, draft: 5 }
|
statusCounts = null, // { all: 10, publish: 5, draft: 5 }
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
searchable = true,
|
searchable = true,
|
||||||
searchPlaceholder = __('Search...', 'formipay'),
|
searchPlaceholder = __('Search...', 'formipay'),
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
sortable = true,
|
sortable = true,
|
||||||
defaultSort = { id: 'ID', desc: true },
|
defaultSort = { id: 'ID', desc: true },
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
selectable = true,
|
selectable = true,
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
pagination = true,
|
pagination = true,
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
pageSizeOptions = [10, 20, 50, 100],
|
pageSizeOptions = [10, 20, 50, 100],
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
actions = {
|
actions = {
|
||||||
addNew: false, // { label, action: 'formipay-create-form-post' }
|
addNew: false, // { label, action: 'formipay-create-form-post' }
|
||||||
bulkDelete: true, // { action: 'formipay-bulk-delete-form' }
|
bulkDelete: true, // { action: 'formipay-bulk-delete-form' }
|
||||||
inline: true, // edit, delete, duplicate
|
inline: true, // edit, delete, duplicate
|
||||||
},
|
},
|
||||||
|
|
||||||
// Empty state
|
// Empty state
|
||||||
emptyMessage = __('No items found', 'formipay'),
|
emptyMessage = __('No items found', 'formipay'),
|
||||||
|
|
||||||
// AJAX config
|
// AJAX config
|
||||||
ajaxUrl,
|
ajaxUrl,
|
||||||
nonce,
|
nonce,
|
||||||
tableAction, // e.g., 'formipay-tabledata-forms'
|
tableAction, // e.g., 'formipay-tabledata-forms'
|
||||||
deleteAction,
|
deleteAction,
|
||||||
duplicateAction,
|
duplicateAction,
|
||||||
|
|
||||||
// Selection callback
|
// Selection callback
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
}) {
|
}) {
|
||||||
// State
|
// State
|
||||||
const [data, setData] = useState(initialData);
|
const [data, setData] = useState(initialData);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
const [activeFilter, setActiveFilter] = useState('all');
|
const [activeFilter, setActiveFilter] = useState('all');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
const [sortBy, setSortBy] = useState(defaultSort.id || 'ID');
|
const [sortBy, setSortBy] = useState(defaultSort.id || 'ID');
|
||||||
const [sortOrder, setSortOrder] = useState(defaultSort.desc ? 'desc' : 'asc');
|
const [sortOrder, setSortOrder] = useState(defaultSort.desc ? 'desc' : 'asc');
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
const [selectedRows, setSelectedRows] = useState(new Set());
|
const [selectedRows, setSelectedRows] = useState(new Set());
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
|
||||||
// Notify parent of selection changes
|
// Notify parent of selection changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onSelectionChange) {
|
if (onSelectionChange) {
|
||||||
onSelectionChange(selectedRows);
|
onSelectionChange(selectedRows);
|
||||||
}
|
}
|
||||||
}, [selectedRows, onSelectionChange]);
|
}, [selectedRows, onSelectionChange]);
|
||||||
|
|
||||||
// Add New Modal
|
// Add New Modal
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [newItemTitle, setNewItemTitle] = useState('');
|
const [newItemTitle, setNewItemTitle] = useState('');
|
||||||
|
|
||||||
// Derive action names from tableAction
|
// Derive action names from tableAction
|
||||||
const baseActionName = tableAction.replace('formipay-tabledata-', '');
|
const baseActionName = tableAction.replace('formipay-tabledata-', '');
|
||||||
const bulkDeleteAction = actions.bulkDelete?.action || `formipay-bulk-delete-${baseActionName}`;
|
const bulkDeleteAction = actions.bulkDelete?.action || `formipay-bulk-delete-${baseActionName}`;
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
action: tableAction,
|
action: tableAction,
|
||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
limit: currentPageSize.toString(),
|
limit: currentPageSize.toString(),
|
||||||
offset: ((currentPage - 1) * currentPageSize).toString(),
|
offset: ((currentPage - 1) * currentPageSize).toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add filter
|
// Add filter
|
||||||
if (filterOptions && activeFilter !== 'all') {
|
if (filterOptions && activeFilter !== 'all') {
|
||||||
params.append(filterOptions.key, activeFilter);
|
params.append(filterOptions.key, activeFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add search
|
// Add search
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
params.append('search', searchQuery);
|
params.append('search', searchQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add sort
|
// Add sort
|
||||||
params.append('orderby', sortBy);
|
params.append('orderby', sortBy);
|
||||||
params.append('sort', sortOrder);
|
params.append('sort', sortOrder);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${ajaxUrl}?${params.toString()}`, {
|
const response = await fetch(`${ajaxUrl}?${params.toString()}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: params,
|
body: params,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
const items = result.data?.results || result.results || result.data || [];
|
const items = result.data?.results || result.results || result.data || [];
|
||||||
setData(items);
|
setData(items);
|
||||||
setTotal(result.total || items.length);
|
setTotal(result.total || items.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Load data error:', error);
|
console.error('Load data error:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [ajaxUrl, nonce, tableAction, currentPageSize, currentPage, activeFilter, searchQuery, sortBy, sortOrder, filterOptions]);
|
}, [ajaxUrl, nonce, tableAction, currentPageSize, currentPage, activeFilter, searchQuery, sortBy, sortOrder, filterOptions]);
|
||||||
|
|
||||||
// Initial load and refresh on filter/sort/page change
|
// Initial load and refresh on filter/sort/page change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
// Handle filter change
|
// Handle filter change
|
||||||
const handleFilterChange = (value) => {
|
const handleFilterChange = (value) => {
|
||||||
setActiveFilter(value);
|
setActiveFilter(value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle search (debounced)
|
// Handle search (debounced)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (searchQuery !== null) {
|
if (searchQuery !== null) {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
// Handle selection
|
// Handle selection
|
||||||
const handleRowSelect = (id) => {
|
const handleRowSelect = (id) => {
|
||||||
const newSelected = new Set(selectedRows);
|
const newSelected = new Set(selectedRows);
|
||||||
if (newSelected.has(id)) {
|
if (newSelected.has(id)) {
|
||||||
newSelected.delete(id);
|
newSelected.delete(id);
|
||||||
} else {
|
} else {
|
||||||
newSelected.add(id);
|
newSelected.add(id);
|
||||||
}
|
}
|
||||||
setSelectedRows(newSelected);
|
setSelectedRows(newSelected);
|
||||||
setSelectAll(false);
|
setSelectAll(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
if (selectAll) {
|
if (selectAll) {
|
||||||
setSelectedRows(new Set());
|
setSelectedRows(new Set());
|
||||||
} else {
|
} else {
|
||||||
setSelectedRows(new Set(data.map(row => row.ID || row.id)));
|
setSelectedRows(new Set(data.map(row => row.ID || row.id)));
|
||||||
}
|
}
|
||||||
setSelectAll(!selectAll);
|
setSelectAll(!selectAll);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle bulk delete
|
// Handle bulk delete
|
||||||
const handleBulkDelete = async () => {
|
const handleBulkDelete = async () => {
|
||||||
if (selectedRows.size === 0) return;
|
if (selectedRows.size === 0) return;
|
||||||
|
|
||||||
const result = await Swal.fire({
|
const confirmed = await confirm({
|
||||||
icon: 'info',
|
title: __('Delete Selected', 'formipay'),
|
||||||
html: __('Do you want to delete the selected item(s)?', 'formipay'),
|
message: __('Do you want to delete the selected item(s)?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Confirm', 'formipay'),
|
||||||
confirmButtonText: __('Confirm', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (confirmed) {
|
||||||
await fetch(`${ajaxUrl}?action=${bulkDeleteAction}`, {
|
await fetch(`${ajaxUrl}?action=${bulkDeleteAction}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
ids: Array.from(selectedRows),
|
ids: Array.from(selectedRows),
|
||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedRows(new Set());
|
setSelectedRows(new Set());
|
||||||
setSelectAll(false);
|
setSelectAll(false);
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
Swal.fire({
|
toast.success(__('Items deleted successfully.', 'formipay'));
|
||||||
title: __('Done!', 'formipay'),
|
}
|
||||||
html: __('Items deleted successfully.', 'formipay'),
|
};
|
||||||
icon: 'success',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle Add New
|
// Handle Add New
|
||||||
const handleAddNew = async () => {
|
const handleAddNew = async () => {
|
||||||
if (!newItemTitle.trim()) {
|
if (!newItemTitle.trim()) {
|
||||||
Swal.fire({
|
toast.error(__('Title is required.', 'formipay'));
|
||||||
html: __('Title is required.', 'formipay'),
|
return;
|
||||||
icon: 'error',
|
}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createAction = actions.addNew.action;
|
const createAction = actions.addNew.action;
|
||||||
const result = await fetch(`${ajaxUrl}?action=${createAction}`, {
|
const result = await fetch(`${ajaxUrl}?action=${createAction}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
title: newItemTitle,
|
title: newItemTitle,
|
||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
setNewItemTitle('');
|
setNewItemTitle('');
|
||||||
if (response.data.edit_post_url) {
|
if (response.data.edit_post_url) {
|
||||||
window.location.href = response.data.edit_post_url;
|
window.location.href = response.data.edit_post_url;
|
||||||
} else {
|
} else {
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
toast.error(response.data.message || __('Error creating item.', 'formipay'));
|
||||||
html: response.data.message || __('Error creating item.', 'formipay'),
|
}
|
||||||
icon: 'error',
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
// Compute total pages
|
||||||
<div className="formipay-data-table-wrapper">
|
const totalPages = Math.ceil(total / currentPageSize);
|
||||||
{/* Toolbar */}
|
|
||||||
<div className="formipay-table-toolbar">
|
|
||||||
{/* Add New Button */}
|
|
||||||
{actions.addNew && (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => setIsAddModalOpen(true)}
|
|
||||||
>
|
|
||||||
{actions.addNew.label || __('+ Add New', 'formipay')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Bulk Delete Button */}
|
return (
|
||||||
{actions.bulkDelete && selectable && selectedRows.size > 0 && (
|
<div className="formipay-design-system">
|
||||||
<Button
|
{/* Toolbar */}
|
||||||
variant="secondary"
|
<div className="flex items-center gap-3 mb-4 flex-wrap">
|
||||||
isDestructive
|
{/* Add New Button */}
|
||||||
onClick={handleBulkDelete}
|
{actions.addNew && (
|
||||||
>
|
<Button
|
||||||
{__('Delete Selected', 'formipay')} ({selectedRows.size})
|
onClick={() => setIsAddModalOpen(true)}
|
||||||
</Button>
|
>
|
||||||
)}
|
{actions.addNew.label || __('+ Add New', 'formipay')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Search */}
|
{/* Bulk Delete Button */}
|
||||||
{searchable && (
|
{actions.bulkDelete && selectable && selectedRows.size > 0 && (
|
||||||
<TextControl
|
<Button
|
||||||
placeholder={searchPlaceholder}
|
variant="destructive"
|
||||||
value={searchQuery}
|
onClick={handleBulkDelete}
|
||||||
onChange={setSearchQuery}
|
>
|
||||||
className="formipay-table-search"
|
{__('Delete Selected', 'formipay')} ({selectedRows.size})
|
||||||
/>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sort */}
|
{/* Search */}
|
||||||
{sortable && (
|
{searchable && (
|
||||||
<SelectControl
|
<Input
|
||||||
value={`${sortBy}-${sortOrder}`}
|
placeholder={searchPlaceholder}
|
||||||
options={[
|
value={searchQuery}
|
||||||
{ label: __('ID ↓', 'formipay'), value: 'ID-desc' },
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
{ label: __('ID ↑', 'formipay'), value: 'ID-asc' },
|
className="max-w-75 grow"
|
||||||
{ label: __('Date ↓', 'formipay'), value: 'date-desc' },
|
/>
|
||||||
{ label: __('Date ↑', 'formipay'), value: 'date-asc' },
|
)}
|
||||||
{ label: __('Title A-Z', 'formipay'), value: 'title-asc' },
|
|
||||||
{ label: __('Title Z-A', 'formipay'), value: 'title-desc' },
|
|
||||||
]}
|
|
||||||
onChange={(value) => {
|
|
||||||
const [id, sort] = value.split('-');
|
|
||||||
setSortBy(id);
|
|
||||||
setSortOrder(sort);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Refresh Button */}
|
{/* Sort */}
|
||||||
<Button
|
{sortable && (
|
||||||
variant="secondary"
|
<Select
|
||||||
onClick={loadData}
|
value={`${sortBy}-${sortOrder}`}
|
||||||
disabled={loading}
|
onValueChange={(value) => {
|
||||||
>
|
const [id, sort] = value.split('-');
|
||||||
{loading ? __('Refreshing...', 'formipay') : __('Refresh', 'formipay')}
|
setSortBy(id);
|
||||||
</Button>
|
setSortOrder(sort);
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ID-desc">{__('ID \u2193', 'formipay')}</SelectItem>
|
||||||
|
<SelectItem value="ID-asc">{__('ID \u2191', 'formipay')}</SelectItem>
|
||||||
|
<SelectItem value="date-desc">{__('Date \u2193', 'formipay')}</SelectItem>
|
||||||
|
<SelectItem value="date-asc">{__('Date \u2191', 'formipay')}</SelectItem>
|
||||||
|
<SelectItem value="title-asc">{__('Title A-Z', 'formipay')}</SelectItem>
|
||||||
|
<SelectItem value="title-desc">{__('Title Z-A', 'formipay')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
{/* Refresh Button */}
|
||||||
{filterOptions && (
|
<Button
|
||||||
<div className="formipay-filter-tabs">
|
variant="outline"
|
||||||
{filterOptions.options.map(option => (
|
onClick={loadData}
|
||||||
<button
|
disabled={loading}
|
||||||
key={option.value}
|
>
|
||||||
className={`filter-tab ${activeFilter === option.value ? 'active' : ''}`}
|
{loading ? __('Refreshing...', 'formipay') : __('Refresh', 'formipay')}
|
||||||
onClick={() => handleFilterChange(option.value)}
|
</Button>
|
||||||
>
|
</div>
|
||||||
{option.label}
|
|
||||||
{statusCounts && (
|
|
||||||
<span className="count">
|
|
||||||
{statusCounts[option.value] || 0}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Table */}
|
{/* Filter Tabs */}
|
||||||
<div className="formipay-table-container">
|
{filterOptions && (
|
||||||
{loading ? (
|
<div className="flex gap-2 mb-4">
|
||||||
<div className="formipay-table-loading">
|
{filterOptions.options.map(option => (
|
||||||
<Spinner />
|
<button
|
||||||
</div>
|
key={option.value}
|
||||||
) : data.length === 0 ? (
|
className={cn(
|
||||||
<div className="formipay-table-empty">
|
'px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
|
||||||
{emptyMessage}
|
activeFilter === option.value
|
||||||
</div>
|
? 'bg-primary text-primary-foreground'
|
||||||
) : (
|
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||||
<table className="formipay-table wp-list-table widefat fixed striped">
|
)}
|
||||||
<thead>
|
onClick={() => handleFilterChange(option.value)}
|
||||||
<tr>
|
>
|
||||||
{/* Checkbox column */}
|
{option.label}
|
||||||
{selectable && (
|
{statusCounts && (
|
||||||
<th className="column-select">
|
<span
|
||||||
<input
|
className={cn(
|
||||||
type="checkbox"
|
'inline-block min-w-4.5 px-1.5 py-0.5 ml-1.5 rounded-full text-[11px] leading-none',
|
||||||
checked={selectAll}
|
activeFilter === option.value
|
||||||
onChange={handleSelectAll}
|
? 'bg-primary-foreground/20 text-primary-foreground'
|
||||||
/>
|
: 'bg-muted-foreground/10 text-muted-foreground'
|
||||||
</th>
|
)}
|
||||||
)}
|
>
|
||||||
{/* Data columns */}
|
{statusCounts[option.value] || 0}
|
||||||
{columns.map((column) => (
|
</span>
|
||||||
<th key={column.key} className={`column-${column.key}`}>
|
)}
|
||||||
{column.label}
|
</button>
|
||||||
</th>
|
))}
|
||||||
))}
|
</div>
|
||||||
</tr>
|
)}
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data.map((row, rowIndex) => {
|
|
||||||
const rowId = row.ID || row.id;
|
|
||||||
return (
|
|
||||||
<tr key={rowIndex} className="formipay-table-row">
|
|
||||||
{/* Checkbox */}
|
|
||||||
{selectable && (
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedRows.has(rowId)}
|
|
||||||
onChange={() => handleRowSelect(rowId)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{/* Data columns */}
|
|
||||||
{columns.map((column) => (
|
|
||||||
<td key={column.key}>
|
|
||||||
{column.render ? column.render(row) : row[column.key]}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Table */}
|
||||||
{pagination && total > currentPageSize && (
|
<div className="rounded-lg border bg-card">
|
||||||
<div className="formipay-table-pagination">
|
{loading ? (
|
||||||
<div className="pagination-info">
|
<div className="flex flex-col items-center justify-center py-12">
|
||||||
{__('Showing', 'formipay')} {((currentPage - 1) * currentPageSize) + 1} - {Math.min(currentPage * currentPageSize, total)} {__('of', 'formipay')} {total}
|
<div className="w-full space-y-3 px-4">
|
||||||
</div>
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<div className="pagination-controls">
|
<div key={i} className="flex items-center gap-4">
|
||||||
<Button
|
<Skeleton className="h-4 w-8" />
|
||||||
variant="secondary"
|
<Skeleton className="h-4 flex-1" />
|
||||||
disabled={currentPage === 1}
|
<Skeleton className="h-4 flex-1" />
|
||||||
onClick={() => setCurrentPage(1)}
|
<Skeleton className="h-4 w-24" />
|
||||||
>
|
</div>
|
||||||
{'««'}
|
))}
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
</div>
|
||||||
variant="secondary"
|
) : data.length === 0 ? (
|
||||||
disabled={currentPage === 1}
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
onClick={() => setCurrentPage(currentPage - 1)}
|
<p>{emptyMessage}</p>
|
||||||
>
|
</div>
|
||||||
{'‹'}
|
) : (
|
||||||
</Button>
|
<Table>
|
||||||
<span className="page-info">
|
<TableHeader>
|
||||||
{__('Page', 'formipay')} {currentPage} {__('of', 'formipay')} {Math.ceil(total / currentPageSize)}
|
<TableRow>
|
||||||
</span>
|
{/* Checkbox column */}
|
||||||
<Button
|
{selectable && (
|
||||||
variant="secondary"
|
<TableHead className="w-10 text-center">
|
||||||
disabled={currentPage >= Math.ceil(total / currentPageSize)}
|
<Checkbox
|
||||||
onClick={() => setCurrentPage(currentPage + 1)}
|
checked={selectAll}
|
||||||
>
|
onCheckedChange={handleSelectAll}
|
||||||
{'›'}
|
/>
|
||||||
</Button>
|
</TableHead>
|
||||||
<Button
|
)}
|
||||||
variant="secondary"
|
{/* Data columns */}
|
||||||
disabled={currentPage >= Math.ceil(total / currentPageSize)}
|
{columns.map((column) => (
|
||||||
onClick={() => setCurrentPage(Math.ceil(total / currentPageSize))}
|
<TableHead key={column.key}>
|
||||||
>
|
{column.label}
|
||||||
{'»'}
|
</TableHead>
|
||||||
</Button>
|
))}
|
||||||
<SelectControl
|
</TableRow>
|
||||||
value={currentPageSize.toString()}
|
</TableHeader>
|
||||||
options={pageSizeOptions.map(size => ({
|
<TableBody>
|
||||||
label: size.toString(),
|
{data.map((row, rowIndex) => {
|
||||||
value: size.toString(),
|
const rowId = row.ID || row.id;
|
||||||
}))}
|
return (
|
||||||
onChange={(value) => {
|
<TableRow key={rowIndex}>
|
||||||
setCurrentPageSize(parseInt(value));
|
{/* Checkbox */}
|
||||||
setCurrentPage(1);
|
{selectable && (
|
||||||
}}
|
<TableCell className="w-10 text-center">
|
||||||
/>
|
<Checkbox
|
||||||
</div>
|
checked={selectedRows.has(rowId)}
|
||||||
</div>
|
onCheckedChange={() => handleRowSelect(rowId)}
|
||||||
)}
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
{/* Data columns */}
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableCell key={column.key}>
|
||||||
|
{column.render ? column.render(row) : row[column.key]}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Add New Modal - only render when open */}
|
{/* Pagination */}
|
||||||
{actions.addNew && isAddModalOpen && (
|
{pagination && total > currentPageSize && (
|
||||||
<Modal
|
<div className="flex items-center justify-between mt-4">
|
||||||
title={actions.addNew.label || __('Add New', 'formipay')}
|
<div className="text-sm text-muted-foreground">
|
||||||
onRequestClose={() => {
|
{__('Showing', 'formipay')} {((currentPage - 1) * currentPageSize) + 1} - {Math.min(currentPage * currentPageSize, total)} {__('of', 'formipay')} {total}
|
||||||
setIsAddModalOpen(false);
|
</div>
|
||||||
setNewItemTitle('');
|
<div className="flex items-center gap-2">
|
||||||
}}
|
<Button
|
||||||
>
|
variant="outline"
|
||||||
<TextControl
|
size="icon"
|
||||||
__next40pxDefaultSize
|
disabled={currentPage === 1}
|
||||||
__nextHasNoMarginBottom
|
onClick={() => setCurrentPage(1)}
|
||||||
label={__('Title', 'formipay')}
|
>
|
||||||
value={newItemTitle}
|
{'\u00AB'}
|
||||||
onChange={setNewItemTitle}
|
</Button>
|
||||||
autoFocus
|
<Button
|
||||||
/>
|
variant="outline"
|
||||||
<div className="formipay-modal-actions">
|
size="icon"
|
||||||
<Button
|
disabled={currentPage === 1}
|
||||||
variant="secondary"
|
onClick={() => setCurrentPage(currentPage - 1)}
|
||||||
onClick={() => {
|
>
|
||||||
setIsAddModalOpen(false);
|
{'\u2039'}
|
||||||
setNewItemTitle('');
|
</Button>
|
||||||
}}
|
<span className="px-2 text-sm text-muted-foreground">
|
||||||
>
|
{__('Page', 'formipay')} {currentPage} {__('of', 'formipay')} {totalPages}
|
||||||
{__('Cancel', 'formipay')}
|
</span>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="primary"
|
size="icon"
|
||||||
onClick={handleAddNew}
|
disabled={currentPage >= totalPages}
|
||||||
>
|
onClick={() => setCurrentPage(currentPage + 1)}
|
||||||
{__('Create', 'formipay')}
|
>
|
||||||
</Button>
|
{'\u203A'}
|
||||||
</div>
|
</Button>
|
||||||
</Modal>
|
<Button
|
||||||
)}
|
variant="outline"
|
||||||
</div>
|
size="icon"
|
||||||
);
|
disabled={currentPage >= totalPages}
|
||||||
|
onClick={() => setCurrentPage(totalPages)}
|
||||||
|
>
|
||||||
|
{'\u00BB'}
|
||||||
|
</Button>
|
||||||
|
<Select
|
||||||
|
value={currentPageSize.toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setCurrentPageSize(parseInt(value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{pageSizeOptions.map(size => (
|
||||||
|
<SelectItem key={size} value={size.toString()}>
|
||||||
|
{size}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add New Dialog */}
|
||||||
|
{actions.addNew && (
|
||||||
|
<Dialog open={isAddModalOpen} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setIsAddModalOpen(false);
|
||||||
|
setNewItemTitle('');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{actions.addNew.label || __('Add New', 'formipay')}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">
|
||||||
|
{__('Title', 'formipay')}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
placeholder={__('Enter title...', 'formipay')}
|
||||||
|
value={newItemTitle}
|
||||||
|
onChange={(e) => setNewItemTitle(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddModalOpen(false);
|
||||||
|
setNewItemTitle('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{__('Cancel', 'formipay')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddNew}
|
||||||
|
>
|
||||||
|
{__('Create', 'formipay')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,76 @@
|
|||||||
/**
|
/**
|
||||||
* Formipay Design System - WPCFTO-inspired React components
|
* Formipay Design System - WPCFTO-inspired React components
|
||||||
* Reusable components matching WPCFTO visual language
|
* Built on shadcn/ui primitives + Tailwind CSS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import './WpcftoDesign.css';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Box Component
|
// shadcn/ui primitives
|
||||||
export function Box({ children, className = '', ...props }) {
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input as ShadcnInput } from '@/components/ui/input';
|
||||||
|
import { Textarea as ShadcnTextarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Select as ShadcnSelect,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Button as ShadcnButton } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertTitle,
|
||||||
|
AlertDescription,
|
||||||
|
} from '@/components/ui/alert';
|
||||||
|
import { Badge as ShadcnBadge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Table as ShadcnTable,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableCell,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
TabsContent,
|
||||||
|
} from '@/components/ui/tabs';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Box
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export function Box({ children, className, ...props }) {
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-box ${className}`} {...props}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'bg-[var(--formipay-color-content-bg,#f0f3f5)] rounded-[10px] mb-2.5 min-h-[80px] shadow-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Box Child Component
|
// ---------------------------------------------------------------------------
|
||||||
export function BoxChild({ children, className = '', ...props }) {
|
// BoxChild
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export function BoxChild({ children, className, ...props }) {
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-box-child ${className}`} {...props}>
|
<div className={cn(className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab Navigation Component - WPCFTO sidebar style
|
// ---------------------------------------------------------------------------
|
||||||
|
// TabNav (WPCFTO sidebar style)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical' }) {
|
export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical' }) {
|
||||||
if (!tabs || !Array.isArray(tabs)) {
|
if (!tabs || !Array.isArray(tabs)) {
|
||||||
console.warn('[Formipay] TabNav: tabs is not an array', tabs);
|
console.warn('[Formipay] TabNav: tabs is not an array', tabs);
|
||||||
@@ -32,32 +78,45 @@ export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical'
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-wpcfto-tab-nav ${orientation === 'vertical' ? 'formipay-wpcfto-sidebar' : ''}`}>
|
<div
|
||||||
<div className="formipay-wpcfto-tab-nav-inner">
|
className={cn(
|
||||||
|
'flex-col w-[273px] h-auto bg-[#2c3e50] rounded-none p-5',
|
||||||
|
orientation !== 'vertical' && 'flex-row w-auto'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<div
|
<div key={tab.id}>
|
||||||
key={tab.id}
|
<TabsTrigger
|
||||||
className={`formipay-wpcfto-nav ${activeTab === tab.id ? 'active' : ''} ${tab.submenu ? 'has-submenu' : ''} ${!tab.icon ? 'no-icon' : ''}`}
|
value={tab.id}
|
||||||
onClick={() => onTabChange(tab.id)}
|
className={cn(
|
||||||
>
|
'w-full justify-start text-[#bec5cb] uppercase text-sm',
|
||||||
<div className="formipay-wpcfto-nav-title">
|
'data-[state=active]:bg-[#2985f7] data-[state=active]:text-white rounded-none',
|
||||||
{tab.icon && <i className={tab.icon}></i>}
|
activeTab === tab.id && 'bg-[#2985f7] text-white'
|
||||||
|
)}
|
||||||
|
onClick={() => onTabChange(tab.id)}
|
||||||
|
>
|
||||||
|
{tab.icon && <i className={tab.icon} />}
|
||||||
<span>{tab.label}</span>
|
<span>{tab.label}</span>
|
||||||
</div>
|
</TabsTrigger>
|
||||||
|
|
||||||
{tab.submenu && (
|
{tab.submenu && (
|
||||||
<div className="formipay-wpcfto-submenus">
|
<div className="flex flex-col mt-1">
|
||||||
{tab.submenu.map((sub) => (
|
{tab.submenu.map((sub) => (
|
||||||
<div
|
<div
|
||||||
key={`${tab.id}_${sub.id}`}
|
key={`${tab.id}_${sub.id}`}
|
||||||
className={`formipay-wpcfto-submenu-item ${activeTab === `${tab.id}_${sub.id}` ? 'active' : ''}`}
|
className={cn(
|
||||||
|
'px-4 py-2 text-sm cursor-pointer text-[#bec5cb] hover:text-white',
|
||||||
|
activeTab === `${tab.id}_${sub.id}` &&
|
||||||
|
'text-white bg-[#2985f7]'
|
||||||
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onTabChange(`${tab.id}_${sub.id}`);
|
onTabChange(`${tab.id}_${sub.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sub.label}
|
{sub.label}
|
||||||
<i className="fa fa-chevron-right"></i>
|
<i className="fa fa-chevron-right ml-auto" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +128,9 @@ export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical'
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab Panel Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// TabPanel
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function TabPanel({ tabs, activeTab, children }) {
|
export function TabPanel({ tabs, activeTab, children }) {
|
||||||
if (!tabs || !Array.isArray(tabs)) {
|
if (!tabs || !Array.isArray(tabs)) {
|
||||||
console.warn('[Formipay] TabPanel: tabs is not an array', tabs);
|
console.warn('[Formipay] TabPanel: tabs is not an array', tabs);
|
||||||
@@ -77,262 +138,352 @@ export function TabPanel({ tabs, activeTab, children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="formipay-tabs">
|
<div className="flex-1">
|
||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`formipay-tab ${tab.id === activeTab ? 'active' : ''}`}
|
className={cn(tab.id !== activeTab && 'hidden')}
|
||||||
>
|
>
|
||||||
<div className="formipay-tab-content">
|
{typeof children === 'function' ? children(tab, index) : children}
|
||||||
{typeof children === 'function' ? children(tab, index) : children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field Component - WPCFTO-style 2-column layout
|
// ---------------------------------------------------------------------------
|
||||||
|
// Field (2-column WPCFTO layout)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function Field({
|
export function Field({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
required = false,
|
required = false,
|
||||||
children,
|
children,
|
||||||
className = '',
|
className,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-generic-field ${className}`} {...props}>
|
<div
|
||||||
<div className="formipay-field-aside">
|
className={cn(
|
||||||
|
'flex flex-wrap justify-between p-[1.8rem_1rem_0] w-full rounded-[10px] bg-card mb-2.5',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<aside className="w-[40%] pr-8">
|
||||||
{label && (
|
{label && (
|
||||||
<div className={`formipay-field-label ${required ? 'required' : ''}`}>
|
<Label className={cn(required && "after:content-['*'] after:ml-0.5 after:text-destructive")}>
|
||||||
<span className="formipay-field-label-text">{label}</span>
|
{label}
|
||||||
</div>
|
</Label>
|
||||||
)}
|
)}
|
||||||
{description && (
|
{description && (
|
||||||
<div className="formipay-field-description">{description}</div>
|
<p className="mt-2 text-[13px] text-muted-foreground">{description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</aside>
|
||||||
<div className="formipay-field-content">
|
<div className="w-[60%]">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// Input
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function Input({
|
export function Input({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
required = false,
|
required = false,
|
||||||
className = '',
|
className,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Field label={label} description={description} required={required}>
|
<Field label={label} description={description} required={required}>
|
||||||
<input className={`formipay-input ${className}`} {...props} />
|
<ShadcnInput className={cn(className)} {...props} />
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textarea Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// Textarea
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function Textarea({
|
export function Textarea({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
required = false,
|
required = false,
|
||||||
rows = 4,
|
rows = 4,
|
||||||
className = '',
|
className,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Field label={label} description={description} required={required}>
|
<Field label={label} description={description} required={required}>
|
||||||
<textarea
|
<ShadcnTextarea rows={rows} className={cn(className)} {...props} />
|
||||||
className={`formipay-textarea ${className}`}
|
|
||||||
rows={rows}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// Select
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function Select({
|
export function Select({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
required = false,
|
required = false,
|
||||||
options = [],
|
options = [],
|
||||||
className = '',
|
className,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
|
// Derive a current value from props for the Radix select
|
||||||
|
const selectValue = props.value ?? props.defaultValue ?? undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field label={label} description={description} required={required}>
|
<Field label={label} description={description} required={required}>
|
||||||
<select className={`formipay-select ${className}`} {...props}>
|
<ShadcnSelect
|
||||||
{options.map((option) => (
|
value={selectValue}
|
||||||
<option key={option.value} value={option.value}>
|
onValueChange={(val) => {
|
||||||
{option.label}
|
if (props.onChange) {
|
||||||
</option>
|
props.onChange({ target: { value: val } });
|
||||||
))}
|
}
|
||||||
</select>
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className={cn('w-full', className)}>
|
||||||
|
<SelectValue placeholder={props.placeholder || __('Select...', 'formipay')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{options.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={String(option.value)}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</ShadcnSelect>
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkbox Component - WPCFTO toggle switch style
|
// ---------------------------------------------------------------------------
|
||||||
|
// Checkbox (toggle-switch style)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function Checkbox({
|
export function Checkbox({
|
||||||
label,
|
label,
|
||||||
checked,
|
checked,
|
||||||
onChange,
|
onChange,
|
||||||
className = '',
|
className,
|
||||||
isToggle = true,
|
isToggle = true,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<label className={`formipay-admin-checkbox ${checked ? 'active' : ''} ${className}`}>
|
<label
|
||||||
<div className={`formipay-admin-checkbox-wrapper ${isToggle ? 'is_toggle' : ''}`}>
|
className={cn(
|
||||||
<div className="formipay-checkbox-switcher"></div>
|
'inline-flex items-center gap-2 cursor-pointer',
|
||||||
<input
|
className
|
||||||
type="checkbox"
|
)}
|
||||||
checked={checked}
|
>
|
||||||
onChange={onChange}
|
<Switch
|
||||||
{...props}
|
checked={checked}
|
||||||
/>
|
onCheckedChange={(val) =>
|
||||||
</div>
|
onChange?.({ target: { checked: val } })
|
||||||
<span>{label}</span>
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{label && <span className="text-sm">{label}</span>}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// Button
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const variantMap = {
|
||||||
|
primary: 'default',
|
||||||
|
secondary: 'secondary',
|
||||||
|
danger: 'destructive',
|
||||||
|
};
|
||||||
|
const sizeMap = {
|
||||||
|
sm: 'sm',
|
||||||
|
md: 'default',
|
||||||
|
lg: 'lg',
|
||||||
|
};
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
children,
|
children,
|
||||||
className = '',
|
className,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onClick,
|
onClick,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const sizeClass = size !== 'md' ? `formipay-btn-${size}` : '';
|
|
||||||
const variantClass = `formipay-btn-${variant}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<ShadcnButton
|
||||||
className={`formipay-btn ${variantClass} ${sizeClass} ${className}`}
|
variant={variantMap[variant] || variant}
|
||||||
|
size={sizeMap[size] || size}
|
||||||
|
className={cn(className)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{Icon && <span className="formipay-btn-icon"><Icon /></span>}
|
{Icon && <Icon />}
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</ShadcnButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repeater Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// Repeater
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function Repeater({
|
export function Repeater({
|
||||||
items,
|
items,
|
||||||
renderItem,
|
renderItem,
|
||||||
onAdd,
|
onAdd,
|
||||||
onRemove,
|
onRemove,
|
||||||
addLabel = 'Add Item',
|
addLabel = 'Add Item',
|
||||||
className = '',
|
className,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-repeater ${className}`}>
|
<div className={cn('flex flex-col gap-3', className)}>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div key={item.id || index} className={`formipay-repeater-item ${item.collapsed ? 'collapsed' : ''}`}>
|
<div
|
||||||
|
key={item.id || index}
|
||||||
|
className={cn(
|
||||||
|
'border rounded-lg overflow-hidden',
|
||||||
|
item.collapsed && 'border-transparent'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="formipay-repeater-header"
|
className="flex items-center justify-between px-4 py-3 bg-muted/50 cursor-pointer select-none"
|
||||||
onClick={() => onToggle?.(item.id)}
|
onClick={() => onToggle?.(item.id)}
|
||||||
>
|
>
|
||||||
<div className="formipay-repeater-title">
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
<span className="formipay-repeater-toggle">▼</span>
|
<span
|
||||||
|
className={cn(
|
||||||
|
'transition-transform text-xs',
|
||||||
|
item.collapsed && '-rotate-90'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</span>
|
||||||
<span>{item.title || `Item ${index + 1}`}</span>
|
<span>{item.title || `Item ${index + 1}`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="formipay-repeater-actions">
|
<button
|
||||||
<span
|
type="button"
|
||||||
className="formipay-repeater-delete"
|
className="text-muted-foreground hover:text-destructive text-sm px-1"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onRemove?.(item.id, index);
|
onRemove?.(item.id, index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</span>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="formipay-repeater-body">
|
|
||||||
{renderItem(item, index)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{!item.collapsed && (
|
||||||
|
<div className="p-4">{renderItem(item, index)}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button className="formipay-repeater-add" onClick={onAdd}>
|
<ShadcnButton
|
||||||
<span>+</span>
|
variant="outline"
|
||||||
<span>{addLabel}</span>
|
size="sm"
|
||||||
</button>
|
className="self-start"
|
||||||
|
onClick={onAdd}
|
||||||
|
>
|
||||||
|
<span className="mr-1">+</span>
|
||||||
|
{addLabel}
|
||||||
|
</ShadcnButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notice Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// Notice
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const noticeStyles = {
|
||||||
|
success: 'border-l-4 border-l-green-500 bg-green-50 text-green-900',
|
||||||
|
warning: 'border-l-4 border-l-yellow-500 bg-yellow-50 text-yellow-900',
|
||||||
|
error: 'border-l-4 border-l-red-500 bg-red-50 text-red-900',
|
||||||
|
info: 'border-l-4 border-l-blue-500 bg-blue-50 text-blue-900',
|
||||||
|
};
|
||||||
|
|
||||||
|
const noticeIcons = {
|
||||||
|
success: '\u2713',
|
||||||
|
warning: '\u26A0',
|
||||||
|
error: '\u2715',
|
||||||
|
info: '\u2139',
|
||||||
|
};
|
||||||
|
|
||||||
export function Notice({
|
export function Notice({
|
||||||
type = 'info',
|
type = 'info',
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
className = '',
|
className,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-notice formipay-notice-${type} ${className}`}>
|
<Alert className={cn(noticeStyles[type] || noticeStyles.info, className)}>
|
||||||
<div className="formipay-notice-icon">
|
<span className="absolute left-4 top-4 text-base font-bold">
|
||||||
{type === 'success' && '✓'}
|
{noticeIcons[type]}
|
||||||
{type === 'warning' && '⚠'}
|
</span>
|
||||||
{type === 'error' && '✕'}
|
<div className="pl-6">
|
||||||
{type === 'info' && 'ℹ'}
|
{title && <AlertTitle>{title}</AlertTitle>}
|
||||||
</div>
|
<AlertDescription>{children}</AlertDescription>
|
||||||
<div className="formipay-notice-content">
|
|
||||||
{title && <div className="formipay-notice-title">{title}</div>}
|
|
||||||
<div>{children}</div>
|
|
||||||
</div>
|
</div>
|
||||||
{onDismiss && (
|
{onDismiss && (
|
||||||
<button
|
<button
|
||||||
className="formipay-notice-dismiss"
|
type="button"
|
||||||
|
className="absolute right-3 top-3 text-muted-foreground hover:text-foreground"
|
||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
aria-label="Dismiss"
|
aria-label="Dismiss"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group Title Component - WPCFTO section divider (visual, not accordion)
|
// ---------------------------------------------------------------------------
|
||||||
export function GroupTitle({
|
// GroupTitle
|
||||||
title,
|
// ---------------------------------------------------------------------------
|
||||||
icon,
|
export function GroupTitle({ title, icon, className }) {
|
||||||
className = '',
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-group-title ${className}`}>
|
<div
|
||||||
{icon && <i className={icon}></i>}
|
className={cn(
|
||||||
|
'w-full pb-3 text-muted-foreground text-sm uppercase tracking-wider border-b flex items-center gap-2 mb-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{icon && <i className={icon} />}
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading Spinner Component
|
// ---------------------------------------------------------------------------
|
||||||
export function Spinner({ size = 'md', className = '' }) {
|
// Spinner
|
||||||
const sizeClass = size !== 'md' ? `formipay-spinner-${size}` : '';
|
// ---------------------------------------------------------------------------
|
||||||
|
export function Spinner({ size = 'md', className }) {
|
||||||
|
const sizeClass = {
|
||||||
|
sm: 'h-4 w-4',
|
||||||
|
md: 'h-6 w-6',
|
||||||
|
lg: 'h-8 w-8',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-loading ${className}`}>
|
<div className={cn('flex items-center justify-center', className)}>
|
||||||
<div className={`formipay-spinner ${sizeClass}`}></div>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'animate-spin rounded-full border-2 border-muted border-t-primary',
|
||||||
|
sizeClass[size] || sizeClass.md
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty State Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// EmptyState
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function EmptyState({
|
export function EmptyState({
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
@@ -340,96 +491,133 @@ export function EmptyState({
|
|||||||
action,
|
action,
|
||||||
actionLabel,
|
actionLabel,
|
||||||
onAction,
|
onAction,
|
||||||
className = '',
|
className,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-empty-state ${className}`}>
|
<div
|
||||||
{icon && <div className="formipay-empty-icon">{icon}</div>}
|
className={cn(
|
||||||
{title && <div className="formipay-empty-title">{title}</div>}
|
'flex flex-col items-center justify-center py-12 text-muted-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{icon && <div className="mb-4 text-4xl">{icon}</div>}
|
||||||
|
{title && <h3 className="text-lg font-semibold mb-1">{title}</h3>}
|
||||||
{description && (
|
{description && (
|
||||||
<div className="formipay-empty-description">{description}</div>
|
<p className="text-sm text-muted-foreground mb-4">{description}</p>
|
||||||
)}
|
)}
|
||||||
{action && (
|
{action && (
|
||||||
<Button onClick={onAction}>
|
<ShadcnButton onClick={onAction}>
|
||||||
{actionLabel || __('Take Action', 'formipay')}
|
{actionLabel || __('Take Action', 'formipay')}
|
||||||
</Button>
|
</ShadcnButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status Badge Component
|
// ---------------------------------------------------------------------------
|
||||||
export function Badge({
|
// Badge
|
||||||
variant = 'default',
|
// ---------------------------------------------------------------------------
|
||||||
children,
|
export function Badge({ variant = 'default', children, className }) {
|
||||||
className = '',
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<span className={`formipay-badge formipay-badge-${variant} ${className}`}>
|
<ShadcnBadge variant={variant} className={cn(className)}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</ShadcnBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table Component
|
// ---------------------------------------------------------------------------
|
||||||
|
// Table
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function Table({
|
export function Table({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
emptyMessage = __('No items found', 'formipay'),
|
emptyMessage = __('No items found', 'formipay'),
|
||||||
className = '',
|
className,
|
||||||
}) {
|
}) {
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-table-wrapper ${className}`}>
|
<div className={cn('flex flex-col items-center justify-center py-12 text-muted-foreground', className)}>
|
||||||
<EmptyState
|
<EmptyState title={emptyMessage} />
|
||||||
title={emptyMessage}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`formipay-table-wrapper ${className}`}>
|
<div className={cn('rounded-lg border', className)}>
|
||||||
<table className="formipay-table">
|
<ShadcnTable>
|
||||||
<thead>
|
<TableHeader>
|
||||||
<tr>
|
<TableRow>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<th key={column.key}>{column.label}</th>
|
<TableHead key={column.key}>{column.label}</TableHead>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</TableRow>
|
||||||
</thead>
|
</TableHeader>
|
||||||
<tbody>
|
<TableBody>
|
||||||
{data.map((row, rowIndex) => (
|
{data.map((row, rowIndex) => (
|
||||||
<tr key={rowIndex}>
|
<TableRow key={rowIndex}>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<td key={column.key}>
|
<TableCell key={column.key}>
|
||||||
{column.render ? column.render(row, rowIndex) : row[column.key]}
|
{column.render
|
||||||
</td>
|
? column.render(row, rowIndex)
|
||||||
|
: row[column.key]}
|
||||||
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</TableBody>
|
||||||
</table>
|
</ShadcnTable>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metabox Layout Component - WPCFTO 2-column wrapper for React metabox islands
|
// ---------------------------------------------------------------------------
|
||||||
|
// MetaboxLayout
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export function MetaboxLayout({ tabs, activeTab, onTabChange, children }) {
|
export function MetaboxLayout({ tabs, activeTab, onTabChange, children }) {
|
||||||
return (
|
return (
|
||||||
<div className="formipay-wpcfto-metabox">
|
<div className="bg-white rounded-[10px] overflow-hidden shadow-sm">
|
||||||
<div className="formipay-wpcfto-metabox-inner">
|
<Tabs
|
||||||
<div className="formipay-wpcfto-container">
|
value={activeTab}
|
||||||
<TabNav tabs={tabs} activeTab={activeTab} onTabChange={onTabChange} />
|
onValueChange={onTabChange}
|
||||||
<div className="formipay-wpcfto-tabs">
|
className="flex flex-row"
|
||||||
{children}
|
>
|
||||||
</div>
|
<TabsList className="flex-col w-[273px] h-auto bg-[#2c3e50] rounded-none p-5">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={tab.id}
|
||||||
|
value={tab.id}
|
||||||
|
className={cn(
|
||||||
|
'w-full justify-start text-[#bec5cb] uppercase text-sm',
|
||||||
|
'data-[state=active]:bg-[#2985f7] data-[state=active]:text-white rounded-none'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.icon && <i className={tab.icon} />}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabsContent
|
||||||
|
key={tab.id}
|
||||||
|
value={tab.id}
|
||||||
|
className="p-6 mt-0"
|
||||||
|
>
|
||||||
|
{typeof children === 'function'
|
||||||
|
? children(tab)
|
||||||
|
: children}
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Default export (aggregate)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
export default {
|
export default {
|
||||||
Box,
|
Box,
|
||||||
BoxChild,
|
BoxChild,
|
||||||
|
|||||||
@@ -4,25 +4,24 @@
|
|||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import DataTable from '../components/shared/DataTable';
|
import DataTable from '../components/shared/DataTable';
|
||||||
|
import { confirm } from '@/lib/confirm';
|
||||||
|
import { toast } from '@/lib/toast';
|
||||||
import './AdminPages.css';
|
import './AdminPages.css';
|
||||||
|
|
||||||
// SweetAlert2 is loaded via WordPress (global scope)
|
|
||||||
const Swal = window.Swal;
|
|
||||||
|
|
||||||
export default function AccessPage() {
|
export default function AccessPage() {
|
||||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||||
const nonce = window.formipayAdmin?.nonce || '';
|
const nonce = window.formipayAdmin?.nonce || '';
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Delete Item', 'formipay'),
|
||||||
html: __('Do you want to delete this item?', 'formipay'),
|
message: __('Do you want to delete this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Delete Permanently', 'formipay'),
|
||||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-delete-access-item`, {
|
await fetch(`${ajaxUrl}?action=formipay-delete-access-item`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -32,20 +31,20 @@ export default function AccessPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item deleted successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDuplicate = async (id) => {
|
const handleDuplicate = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Duplicate Item', 'formipay'),
|
||||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
message: __('Do you want to duplicate this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Confirm', 'formipay'),
|
||||||
confirmButtonText: __('Confirm', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-duplicate-access-item`, {
|
await fetch(`${ajaxUrl}?action=formipay-duplicate-access-item`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -55,6 +54,7 @@ export default function AccessPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item duplicated successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import DataTable from '../components/shared/DataTable';
|
import DataTable from '../components/shared/DataTable';
|
||||||
|
import { confirm } from '@/lib/confirm';
|
||||||
|
import { toast } from '@/lib/toast';
|
||||||
import './AdminPages.css';
|
import './AdminPages.css';
|
||||||
|
|
||||||
// SweetAlert2 is loaded via WordPress (global scope)
|
|
||||||
const Swal = window.Swal;
|
|
||||||
|
|
||||||
export default function CouponsPage() {
|
export default function CouponsPage() {
|
||||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||||
const nonce = window.formipayAdmin?.nonce || '';
|
const nonce = window.formipayAdmin?.nonce || '';
|
||||||
@@ -19,15 +18,15 @@ export default function CouponsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Delete Item', 'formipay'),
|
||||||
html: __('Do you want to delete this item?', 'formipay'),
|
message: __('Do you want to delete this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Delete Permanently', 'formipay'),
|
||||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-delete-coupon`, {
|
await fetch(`${ajaxUrl}?action=formipay-delete-coupon`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -37,20 +36,20 @@ export default function CouponsPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item deleted successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDuplicate = async (id) => {
|
const handleDuplicate = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Duplicate Item', 'formipay'),
|
||||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
message: __('Do you want to duplicate this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Confirm', 'formipay'),
|
||||||
confirmButtonText: __('Confirm', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-duplicate-coupon`, {
|
await fetch(`${ajaxUrl}?action=formipay-duplicate-coupon`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -60,6 +59,7 @@ export default function CouponsPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item duplicated successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,24 +4,23 @@
|
|||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import DataTable from '../components/shared/DataTable';
|
import DataTable from '../components/shared/DataTable';
|
||||||
|
import { confirm } from '@/lib/confirm';
|
||||||
// SweetAlert2 is loaded via WordPress (global scope)
|
import { toast } from '@/lib/toast';
|
||||||
const Swal = window.Swal;
|
|
||||||
|
|
||||||
export default function FormsPage() {
|
export default function FormsPage() {
|
||||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||||
const nonce = window.formipayAdmin?.nonce || '';
|
const nonce = window.formipayAdmin?.nonce || '';
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Delete Item', 'formipay'),
|
||||||
html: __('Do you want to delete this item?', 'formipay'),
|
message: __('Do you want to delete this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Delete Permanently', 'formipay'),
|
||||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-delete-form`, {
|
await fetch(`${ajaxUrl}?action=formipay-delete-form`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -31,20 +30,20 @@ export default function FormsPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item deleted successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDuplicate = async (id) => {
|
const handleDuplicate = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Duplicate Item', 'formipay'),
|
||||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
message: __('Do you want to duplicate this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Confirm', 'formipay'),
|
||||||
confirmButtonText: __('Confirm', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-duplicate-form`, {
|
await fetch(`${ajaxUrl}?action=formipay-duplicate-form`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -54,6 +53,7 @@ export default function FormsPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item duplicated successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -155,15 +155,7 @@ export default function FormsPage() {
|
|||||||
e.currentTarget.innerHTML = originalHTML;
|
e.currentTarget.innerHTML = originalHTML;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
Swal.fire({
|
toast.success(__('Shortcode copied!', 'formipay'));
|
||||||
icon: 'success',
|
|
||||||
title: __('Shortcode copied!', 'formipay'),
|
|
||||||
toast: true,
|
|
||||||
position: 'top-end',
|
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 3000,
|
|
||||||
timerProgressBar: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,25 +4,24 @@
|
|||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import DataTable from '../components/shared/DataTable';
|
import DataTable from '../components/shared/DataTable';
|
||||||
|
import { confirm } from '@/lib/confirm';
|
||||||
|
import { toast } from '@/lib/toast';
|
||||||
import './AdminPages.css';
|
import './AdminPages.css';
|
||||||
|
|
||||||
// SweetAlert2 is loaded via WordPress (global scope)
|
|
||||||
const Swal = window.Swal;
|
|
||||||
|
|
||||||
export default function LicensesPage() {
|
export default function LicensesPage() {
|
||||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||||
const nonce = window.formipayAdmin?.nonce || '';
|
const nonce = window.formipayAdmin?.nonce || '';
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Delete Item', 'formipay'),
|
||||||
html: __('Do you want to delete this item?', 'formipay'),
|
message: __('Do you want to delete this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Delete Permanently', 'formipay'),
|
||||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-delete-license`, {
|
await fetch(`${ajaxUrl}?action=formipay-delete-license`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -32,6 +31,7 @@ export default function LicensesPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item deleted successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import { __ } from '@wordpress/i18n';
|
|||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import DataTable from '../components/shared/DataTable';
|
import DataTable from '../components/shared/DataTable';
|
||||||
import VariationPricingTable from '../components/products/VariationPricingTable';
|
import VariationPricingTable from '../components/products/VariationPricingTable';
|
||||||
|
import { confirm } from '@/lib/confirm';
|
||||||
|
import { toast } from '@/lib/toast';
|
||||||
import './AdminPages.css';
|
import './AdminPages.css';
|
||||||
|
|
||||||
// SweetAlert2 is loaded via WordPress (global scope)
|
|
||||||
const Swal = window.Swal;
|
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function ProductsPage() {
|
||||||
const [isEditor, setIsEditor] = useState(false);
|
const [isEditor, setIsEditor] = useState(false);
|
||||||
const [selectedProductId, setSelectedProductId] = useState(null);
|
const [selectedProductId, setSelectedProductId] = useState(null);
|
||||||
@@ -19,15 +18,15 @@ export default function ProductsPage() {
|
|||||||
const nonce = window.formipayAdmin?.nonce || '';
|
const nonce = window.formipayAdmin?.nonce || '';
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Delete Item', 'formipay'),
|
||||||
html: __('Do you want to delete this item?', 'formipay'),
|
message: __('Do you want to delete this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Delete Permanently', 'formipay'),
|
||||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-delete-product`, {
|
await fetch(`${ajaxUrl}?action=formipay-delete-product`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -37,20 +36,20 @@ export default function ProductsPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item deleted successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDuplicate = async (id) => {
|
const handleDuplicate = async (id) => {
|
||||||
const result = await Swal.fire({
|
const result = await confirm({
|
||||||
icon: 'info',
|
title: __('Duplicate Item', 'formipay'),
|
||||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
message: __('Do you want to duplicate this item?', 'formipay'),
|
||||||
showCancelButton: true,
|
confirmText: __('Confirm', 'formipay'),
|
||||||
confirmButtonText: __('Confirm', 'formipay'),
|
cancelText: __('Cancel', 'formipay'),
|
||||||
cancelButtonText: __('Cancel', 'formipay'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result) {
|
||||||
await fetch(`${ajaxUrl}?action=formipay-duplicate-product`, {
|
await fetch(`${ajaxUrl}?action=formipay-duplicate-product`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -60,6 +59,7 @@ export default function ProductsPage() {
|
|||||||
_wpnonce: nonce,
|
_wpnonce: nonce,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
toast.success(__('Item duplicated successfully.', 'formipay'));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user