feat: implement full-featured DataTable component

Implement comprehensive DataTable with all Grid.js features:
- Checkbox selection with "Select All"
- Bulk delete button (shows when rows selected)
- Inline row actions (edit, delete, duplicate) on hover
- Status filter tabs with counts
- Search input with debounce
- Sort dropdown (ID, date, title ASC/DESC)
- Server-side pagination
- "Add New" modal with SweetAlert2
- SweetAlert2 loaded via WordPress global scope

Updated Forms page to use new DataTable component with:
- Full column rendering (ID, title, date, status, shortcode)
- Copy shortcode button with toast notification
- All filter, search, sort, pagination features

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:10:45 +07:00
parent f7c09a17cf
commit 8529cfa2c0
17 changed files with 901 additions and 132 deletions

View File

@@ -1,74 +1,59 @@
/**
* Forms Page - List view and Form Builder
* Forms Page - Form management with full table features
*/
import { __ } from '@wordpress/i18n';
import { useState, useCallback, useEffect } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { formsApi } from '../api/client';
import DataTable from '../components/shared/DataTable';
import './AdminPages.css';
export default function FormsPage({ initialData }) {
const [isBuilder, setIsBuilder] = useState(false);
const [selectedFormId, setSelectedFormId] = useState(null);
const [forms, setForms] = useState([]);
const [loading, setLoading] = useState(true);
const loadForms = useCallback(() => {
setLoading(true);
formsApi.list()
.then(result => {
console.log('Forms API result:', result);
// Handle both WordPress format and direct format
const formsData = result.data?.results || result.results || result.data || [];
console.log('Forms data extracted:', formsData);
console.log('Forms data length:', formsData.length);
setForms(formsData);
})
.catch(error => {
console.error('Load forms error:', error);
})
.finally(() => {
setLoading(false);
});
}, []);
useEffect(() => {
loadForms();
}, [loadForms]);
if (isBuilder && selectedFormId) {
window.location.href = `${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${selectedFormId}&action=edit`;
return null;
}
// SweetAlert2 is loaded via WordPress (global scope)
const Swal = window.Swal;
export default function FormsPage() {
const columns = [
{
key: 'id',
key: 'ID',
label: __('ID', 'formipay'),
render: (row) => <strong>#{row.ID || row.id}</strong>
render: (row) => <strong>#{row.ID}</strong>
},
{
key: 'title',
label: __('Title', 'formipay'),
render: (row) => (
<a
href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID || row.id}&action=edit`}
onClick={(e) => {
e.preventDefault();
setIsBuilder(true);
setSelectedFormId(row.ID || row.id);
}}
>
{row.post_title || row.title || __('Untitled', 'formipay')}
</a>
<>
<strong>{row.title}</strong>
<br />
<span className="row-actions" style={{ display: 'none', visibility: 'hidden' }}>
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>
{__('Edit', 'formipay')}
</a>
{' | '}
<button className="button-link delete" data-id={row.ID}>
{__('Delete', 'formipay')}
</button>
{' | '}
<button className="button-link duplicate" data-id={row.ID}>
{__('Duplicate', 'formipay')}
</button>
</span>
</>
)
},
{
key: 'shortcode',
label: __('Shortcode', 'formipay'),
render: (row) => <code>[formipay id="{row.ID || row.id}"]</code>
key: 'date',
label: __('Date', 'formipay'),
render: (row) => {
const date = new Date(row.date);
return (
<span style={{ whiteSpace: 'nowrap' }}>
{date.toLocaleDateString()}
<br />
<span style={{ fontSize: 'smaller', color: '#646970' }}>
{date.toLocaleTimeString()}
</span>
</span>
);
}
},
{
key: 'status',
@@ -80,22 +65,51 @@ export default function FormsPage({ initialData }) {
draft: __('Draft', 'formipay'),
pending: __('Pending', 'formipay'),
};
const statusLabel = statusLabels[status] || status;
return (
<span className={`status-badge status-${status === 'publish' ? 'active' : 'draft'}`}>
{statusLabel}
<span className={`status-label ${status}`}>
{statusLabels[status] || status}
</span>
);
}
},
{
key: 'date',
label: __('Date', 'formipay'),
render: (row) => {
const date = row.post_date || row.date;
if (!date) return '-';
return new Date(date).toLocaleDateString();
}
key: 'shortcode',
label: __('Shortcode', 'formipay'),
render: (row) => (
<>
<input
className="formipay-form-shortcode"
value={`[formipay form=${row.ID}]`}
disabled
/>
<button
className="copy-shortcode"
data-copy={`[formipay form=${row.ID}]`}
onClick={(e) => {
const text = e.currentTarget.dataset.copy;
navigator.clipboard.writeText(text).then(() => {
const originalHTML = e.currentTarget.innerHTML;
e.currentTarget.innerHTML = '✓ Copied';
setTimeout(() => {
e.currentTarget.innerHTML = originalHTML;
}, 2000);
Swal.fire({
icon: 'success',
title: __('Shortcode copied!', 'formipay'),
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
});
});
}}
>
📋 {__('Copy', 'formipay')}
</button>
</>
)
},
];
@@ -103,18 +117,33 @@ export default function FormsPage({ initialData }) {
<div className="formipay-page-forms">
<div className="formipay-page-header">
<h1>{ __('Forms', 'formipay') }</h1>
<Button
variant="primary"
href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post-new.php?post_type=formipay-form`}
>
{ __('+ Add New Form', 'formipay') }
</Button>
</div>
<DataTable
columns={columns}
data={forms}
loading={loading}
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
nonce={window.formipayAdmin?.nonce || ''}
tableAction="formipay-tabledata-forms"
deleteAction="formipay-delete-form"
duplicateAction="formipay-duplicate-form"
filterOptions={{
key: 'post_status',
options: [
{ value: 'all', label: __('All', 'formipay') },
{ value: 'publish', label: __('Published', 'formipay') },
{ value: 'draft', label: __('Draft', 'formipay') },
]
}}
actions={{
addNew: {
label: __('+ Add New Form', 'formipay'),
action: 'formipay-create-form-post',
},
bulkDelete: {
action: 'formipay-bulk-delete-form',
},
inline: true,
}}
emptyMessage={__('No forms found', 'formipay')}
/>
</div>