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,26 +1,269 @@
.formipay-data-table-loading,
.formipay-data-table-empty {
padding: 40px;
text-align: center;
/* DataTable Wrapper */
.formipay-data-table-wrapper {
margin: 20px 0;
}
.formipay-data-table {
margin-top: 20px;
/* Toolbar */
.formipay-table-toolbar {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 16px;
}
.formipay-data-table thead th {
padding: 12px 10px;
.formipay-table-search {
max-width: 300px;
flex-grow: 1;
}
.formipay-table-toolbar .components-select-control {
min-width: 150px;
}
/* Filter Tabs */
.formipay-filter-tabs {
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 1px solid #ddd;
}
.formipay-filter-tabs .filter-tab {
padding: 8px 16px;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: 13px;
color: #646970;
transition: all 0.2s;
}
.formipay-filter-tabs .filter-tab:hover {
color: #135e96;
background: #f0f0f1;
}
.formipay-filter-tabs .filter-tab.active {
color: #135e96;
border-bottom-color: #135e96;
font-weight: 600;
}
.formipay-data-table tbody td {
padding: 10px;
.formipay-filter-tabs .filter-tab .count {
display: inline-block;
min-width: 18px;
padding: 2px 6px;
margin-left: 6px;
background: #dcdcde;
border-radius: 10px;
font-size: 11px;
line-height: 1.4;
}
.formipay-data-table tbody tr.is-clickable {
.formipay-filter-tabs .filter-tab.active .count {
background: #135e96;
color: #fff;
}
/* Table Container */
.formipay-table-container {
background: #fff;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.formipay-table-loading {
padding: 60px;
text-align: center;
}
.formipay-table-empty {
padding: 40px;
text-align: center;
color: #646970;
}
/* Table */
.formipay-table {
width: 100%;
border-collapse: collapse;
}
.formipay-table thead th {
padding: 12px 10px;
font-weight: 600;
text-align: left;
border-bottom: 1px solid #c3c4c7;
background: #f6f7f7;
}
.formipay-table tbody td {
padding: 10px;
border-bottom: 1px solid #c3c4c7;
}
.formipay-table tbody tr:last-child td {
border-bottom: none;
}
.formipay-table tbody tr:hover {
background-color: #f0f0f1;
}
/* Checkbox Column */
.formipay-table .column-select {
width: 40px;
text-align: center;
}
.formipay-table tbody td:first-child input[type="checkbox"] {
margin: 0;
}
/* Actions Column */
.formipay-table .column-actions {
width: 200px;
}
.formipay-table .row-actions {
display: none;
visibility: hidden;
}
.formipay-table tbody tr:hover .row-actions {
display: block;
visibility: visible;
}
.formipay-table .row-actions a,
.formipay-table .row-actions .button-link {
text-decoration: none;
color: #a7aaad;
cursor: pointer;
}
.formipay-data-table tbody tr.is-clickable:hover {
background-color: #f0f0f1;
.formipay-table .row-actions a:hover,
.formipay-table .row-actions .button-link:hover {
color: #135e96;
}
.formipay-table .row-actions .delete {
color: #b32d2e;
}
.formipay-table .row-actions .delete:hover {
color: #d63638;
}
/* Status Labels */
.formipay-table .status-label {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.formipay-table .status-label.publish {
background: #edfaef;
color: #007017;
}
.formipay-table .status-label.draft {
background: #f0f0f1;
color: #646970;
}
.formipay-table .status-label.pending {
background: #fff8e5;
color: #d63638;
}
/* Shortcode Input */
.formipay-table input.formipay-form-shortcode {
padding: 4px 8px;
border: 1px solid #8c8f94;
border-radius: 4px;
background: #f6f7f7;
color: #646970;
font-family: monospace;
font-size: 12px;
min-width: 150px;
}
.formipay-table button.copy-shortcode {
padding: 4px 8px;
margin-left: 4px;
border: 1px solid #8c8f94;
border-radius: 4px;
background: #fff;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
.formipay-table button.copy-shortcode:hover {
background: #f6f7f7;
}
.formipay-table button.copy-shortcode svg {
width: 16px;
height: 16px;
}
/* Pagination */
.formipay-table-pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #fff;
border: 1px solid #c3c4c7;
border-top: none;
}
.formipay-table-pagination .pagination-info {
color: #646970;
font-size: 13px;
}
.formipay-table-pagination .pagination-controls {
display: flex;
gap: 8px;
align-items: center;
}
.formipay-table-pagination .page-info {
padding: 0 8px;
color: #646970;
font-size: 13px;
}
.formipay-table-pagination .components-select-control {
min-width: 80px;
}
/* Modal Actions */
.formipay-modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
/* Sort Indicator */
.formipay-table thead th.sorted {
position: relative;
padding-right: 20px;
}
.formipay-table thead th .sort-indicator {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
color: #135e96;
}

View File

@@ -1,57 +1,554 @@
/**
* Data Table - Simple table component for admin listings
* Full-featured DataTable component
* Supports: selection, filtering, search, sort, pagination, actions
*/
import { __ } from '@wordpress/i18n';
import { useState, useCallback, useEffect } from '@wordpress/element';
import {
Button,
Modal,
TextControl,
SelectControl,
Spinner,
} from '@wordpress/components';
import './DataTable.css';
export default function DataTable({
columns,
data,
loading,
emptyMessage = __('No items found', 'formipay'),
onRowClick
}) {
if (loading) {
return (
<div className="formipay-data-table-loading">
<span className="spinner is-active" />
</div>
);
}
// SweetAlert2 is loaded via WordPress (global scope)
const Swal = window.Swal;
if (!data || data.length === 0) {
return (
<div className="formipay-data-table-empty">
<p>{ emptyMessage }</p>
</div>
);
}
export default function DataTable({
// Data fetching
initialData = [],
// Columns definition
columns,
// Filtering
filterOptions = null, // { key: 'post_status', options: [{value, label}] }
statusCounts = null, // { all: 10, publish: 5, draft: 5 }
// Search
searchable = true,
searchPlaceholder = __('Search...', 'formipay'),
// Sorting
sortable = true,
defaultSort = { id: 'ID', desc: true },
// Selection
selectable = true,
// Pagination
pagination = true,
pageSize = 10,
pageSizeOptions = [10, 20, 50, 100],
// Actions
actions = {
addNew: false, // { label, action: 'formipay-create-form-post' }
bulkDelete: true, // { action: 'formipay-bulk-delete-form' }
inline: true, // edit, delete, duplicate
},
// Empty state
emptyMessage = __('No items found', 'formipay'),
// AJAX config
ajaxUrl,
nonce,
tableAction, // e.g., 'formipay-tabledata-forms'
deleteAction,
duplicateAction,
}) {
// State
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0);
// Filters
const [activeFilter, setActiveFilter] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
// Sorting
const [sortBy, setSortBy] = useState(defaultSort.id || 'ID');
const [sortOrder, setSortOrder] = useState(defaultSort.desc ? 'desc' : 'asc');
// Pagination
const [currentPage, setCurrentPage] = useState(1);
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
// Selection
const [selectedRows, setSelectedRows] = useState(new Set());
const [selectAll, setSelectAll] = useState(false);
// Add New Modal
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [newItemTitle, setNewItemTitle] = useState('');
// Derive action names from tableAction
const baseActionName = tableAction.replace('formipay-tabledata-', '');
const bulkDeleteAction = actions.bulkDelete?.action || `formipay-bulk-delete-${baseActionName}`;
const deleteActionName = deleteAction || `formipay-delete-${baseActionName}`;
const duplicateActionName = duplicateAction || `formipay-duplicate-${baseActionName}`;
// Load data
const loadData = useCallback(async () => {
setLoading(true);
const params = new URLSearchParams({
action: tableAction,
_wpnonce: nonce,
limit: currentPageSize.toString(),
offset: ((currentPage - 1) * currentPageSize).toString(),
});
// Add filter
if (filterOptions && activeFilter !== 'all') {
params.append(filterOptions.key, activeFilter);
}
// Add search
if (searchQuery) {
params.append('search', searchQuery);
}
// Add sort
params.append('orderby', sortBy);
params.append('sort', sortOrder);
try {
const response = await fetch(`${ajaxUrl}?${params.toString()}`, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
});
const result = await response.json();
const items = result.data?.results || result.results || result.data || [];
setData(items);
setTotal(result.total || items.length);
} catch (error) {
console.error('Load data error:', error);
} finally {
setLoading(false);
}
}, [ajaxUrl, nonce, tableAction, currentPageSize, currentPage, activeFilter, searchQuery, sortBy, sortOrder, filterOptions]);
// Initial load and refresh on filter/sort/page change
useEffect(() => {
loadData();
}, [loadData]);
// Handle filter change
const handleFilterChange = (value) => {
setActiveFilter(value);
setCurrentPage(1);
};
// Handle search (debounced)
useEffect(() => {
const timeoutId = setTimeout(() => {
if (searchQuery !== null) {
setCurrentPage(1);
}
}, 500);
return () => clearTimeout(timeoutId);
}, [searchQuery]);
// Handle selection
const handleRowSelect = (id) => {
const newSelected = new Set(selectedRows);
if (newSelected.has(id)) {
newSelected.delete(id);
} else {
newSelected.add(id);
}
setSelectedRows(newSelected);
setSelectAll(false);
};
const handleSelectAll = () => {
if (selectAll) {
setSelectedRows(new Set());
} else {
setSelectedRows(new Set(data.map(row => row.ID || row.id)));
}
setSelectAll(!selectAll);
};
// Handle bulk delete
const handleBulkDelete = async () => {
if (selectedRows.size === 0) return;
const result = await Swal.fire({
icon: 'info',
html: __('Do you want to delete the selected item(s)?', 'formipay'),
showCancelButton: true,
confirmButtonText: __('Confirm', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'),
});
if (result.isConfirmed) {
await fetch(`${ajaxUrl}?action=${bulkDeleteAction}`, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
ids: Array.from(selectedRows),
_wpnonce: nonce,
}),
});
setSelectedRows(new Set());
setSelectAll(false);
loadData();
Swal.fire({
title: __('Done!', 'formipay'),
html: __('Items deleted successfully.', 'formipay'),
icon: 'success',
});
}
};
// Handle inline delete
const handleDelete = async (id) => {
const result = await Swal.fire({
icon: 'info',
html: __('Do you want to delete this item?', 'formipay'),
showCancelButton: true,
confirmButtonText: __('Delete Permanently', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'),
});
if (result.isConfirmed) {
await fetch(`${ajaxUrl}?action=${deleteActionName}`, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
id,
_wpnonce: nonce,
}),
});
loadData();
}
};
// Handle duplicate
const handleDuplicate = async (id) => {
const result = await Swal.fire({
icon: 'info',
html: __('Do you want to duplicate this item?', 'formipay'),
showCancelButton: true,
confirmButtonText: __('Confirm', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'),
});
if (result.isConfirmed) {
await fetch(`${ajaxUrl}?action=${duplicateActionName}`, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
id,
_wpnonce: nonce,
}),
});
loadData();
}
};
// Handle Add New
const handleAddNew = async () => {
if (!newItemTitle.trim()) {
Swal.fire({
html: __('Title is required.', 'formipay'),
icon: 'error',
});
return;
}
const createAction = actions.addNew.action;
const result = await fetch(`${ajaxUrl}?action=${createAction}`, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
title: newItemTitle,
_wpnonce: nonce,
}),
});
const response = await result.json();
if (response.success) {
setIsAddModalOpen(false);
setNewItemTitle('');
if (response.data.edit_post_url) {
window.location.href = response.data.edit_post_url;
} else {
loadData();
}
} else {
Swal.fire({
html: response.data.message || __('Error creating item.', 'formipay'),
icon: 'error',
});
}
};
return (
<table className="formipay-data-table wp-list-table widefat fixed striped">
<thead>
<tr>
{columns.map((column) => (
<th key={column.key}>{column.label}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<tr
key={rowIndex}
onClick={onRowClick ? () => onRowClick(row) : undefined}
className={onRowClick ? 'is-clickable' : ''}
<div className="formipay-data-table-wrapper">
{/* Toolbar */}
<div className="formipay-table-toolbar">
{/* Add New Button */}
{actions.addNew && (
<Button
variant="primary"
onClick={() => setIsAddModalOpen(true)}
>
{columns.map((column) => (
<td key={column.key}>
{column.render ? column.render(row) : row[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
{actions.addNew.label || __('+ Add New', 'formipay')}
</Button>
)}
{/* Bulk Delete Button */}
{actions.bulkDelete && selectable && selectedRows.size > 0 && (
<Button
variant="secondary"
isDestructive
onClick={handleBulkDelete}
>
{__('Delete Selected', 'formipay')} ({selectedRows.size})
</Button>
)}
{/* Search */}
{searchable && (
<TextControl
placeholder={searchPlaceholder}
value={searchQuery}
onChange={setSearchQuery}
className="formipay-table-search"
/>
)}
{/* Sort */}
{sortable && (
<SelectControl
value={`${sortBy}-${sortOrder}`}
options={[
{ label: __('ID ↓', 'formipay'), value: 'ID-desc' },
{ label: __('ID ↑', 'formipay'), value: 'ID-asc' },
{ 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);
}}
/>
)}
</div>
{/* Filter Tabs */}
{filterOptions && (
<div className="formipay-filter-tabs">
{filterOptions.options.map(option => (
<button
key={option.value}
className={`filter-tab ${activeFilter === option.value ? 'active' : ''}`}
onClick={() => handleFilterChange(option.value)}
>
{option.label}
{statusCounts && (
<span className="count">
{statusCounts[option.value] || 0}
</span>
)}
</button>
))}
</div>
)}
{/* Table */}
<div className="formipay-table-container">
{loading ? (
<div className="formipay-table-loading">
<Spinner />
</div>
) : data.length === 0 ? (
<div className="formipay-table-empty">
{emptyMessage}
</div>
) : (
<table className="formipay-table wp-list-table widefat fixed striped">
<thead>
<tr>
{/* Checkbox column */}
{selectable && (
<th className="column-select">
<input
type="checkbox"
checked={selectAll}
onChange={handleSelectAll}
/>
</th>
)}
{/* Data columns */}
{columns.map((column) => (
<th key={column.key} className={`column-${column.key}`}>
{column.label}
</th>
))}
{/* Actions column */}
{actions.inline && (
<th className="column-actions">{__('Actions', 'formipay')}</th>
)}
</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>
))}
{/* Actions */}
{actions.inline && (
<td className="column-actions">
<div className="row-actions">
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${rowId}&action=edit`}>
{__('Edit', 'formipay')}
</a>
{' | '}
<button
className="button-link delete"
onClick={() => handleDelete(rowId)}
>
{__('Delete', 'formipay')}
</button>
{' | '}
<button
className="button-link duplicate"
onClick={() => handleDuplicate(rowId)}
>
{__('Duplicate', 'formipay')}
</button>
</div>
</td>
)}
</tr>
);
})}
</tbody>
</table>
)}
</div>
{/* Pagination */}
{pagination && total > currentPageSize && (
<div className="formipay-table-pagination">
<div className="pagination-info">
{__('Showing', 'formipay')} {((currentPage - 1) * currentPageSize) + 1} - {Math.min(currentPage * currentPageSize, total)} {__('of', 'formipay')} {total}
</div>
<div className="pagination-controls">
<Button
variant="secondary"
disabled={currentPage === 1}
onClick={() => setCurrentPage(1)}
>
{'««'}
</Button>
<Button
variant="secondary"
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
>
{''}
</Button>
<span className="page-info">
{__('Page', 'formipay')} {currentPage} {__('of', 'formipay')} {Math.ceil(total / currentPageSize)}
</span>
<Button
variant="secondary"
disabled={currentPage >= Math.ceil(total / currentPageSize)}
onClick={() => setCurrentPage(currentPage + 1)}
>
{''}
</Button>
<Button
variant="secondary"
disabled={currentPage >= Math.ceil(total / currentPageSize)}
onClick={() => setCurrentPage(Math.ceil(total / currentPageSize))}
>
{'»'}
</Button>
<SelectControl
value={currentPageSize.toString()}
options={pageSizeOptions.map(size => ({
label: size.toString(),
value: size.toString(),
}))}
onChange={(value) => {
setCurrentPageSize(parseInt(value));
setCurrentPage(1);
}}
/>
</div>
</div>
)}
{/* Add New Modal */}
{actions.addNew && (
<Modal
title={actions.addNew.label || __('Add New', 'formipay')}
isOpen={isAddModalOpen}
onClose={() => setIsAddModalOpen(false)}
>
<TextControl
label={__('Title', 'formipay')}
value={newItemTitle}
onChange={setNewItemTitle}
autoFocus
/>
<div className="formipay-modal-actions">
<Button
variant="secondary"
onClick={() => setIsAddModalOpen(false)}
>
{__('Cancel', 'formipay')}
</Button>
<Button
variant="primary"
onClick={handleAddNew}
>
{__('Create', 'formipay')}
</Button>
</div>
</Modal>
)}
</div>
);
}

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>