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

@@ -3,6 +3,6 @@
.formipay-order-timeline h3{color:#1e1e1e;font-size:16px;font-weight:600;margin:0 0 16px}.timeline-progress{justify-content:space-between;margin-bottom:24px}.timeline-progress,.timeline-step{align-items:center;display:flex;position:relative}.timeline-step{flex:1;flex-direction:column;gap:8px;z-index:1}.timeline-dot{background:#e0e0e0;border:2px solid #c3c4c7;border-radius:50%;height:24px;position:relative;width:24px}.timeline-step.completed .timeline-dot{background:#2271b1;border-color:#2271b1}.timeline-step.completed .timeline-dot:after{background:#fff;border-radius:50%;content:"";height:8px;right:50%;position:absolute;top:50%;transform:translate(50%,-50%);width:8px}.timeline-line{background:#e0e0e0;height:2px;right:50%;position:absolute;top:12px;width:100%;z-index:-1}.timeline-step.completed .timeline-line{background:#2271b1}.timeline-label{color:#646970;font-size:10px;font-weight:600;text-align:center;text-transform:uppercase}.timeline-events ul{list-style:none;margin:0;padding:0}.timeline-events li{border-bottom:1px solid #f0f0f1;display:grid;gap:8px;grid-template-columns:1fr auto;padding:12px 0}.timeline-events li:last-child{border-bottom:none}.event-status{color:#1e1e1e;font-size:13px;font-weight:600}.event-date{color:#646970;font-size:11px;text-align:left}.event-note{color:#646970;font-size:12px;grid-column:1/-1;margin-top:4px}.no-events{color:#646970;font-size:13px;padding:20px 0;text-align:center}
.formipay-notification-log h3{align-items:center;color:#1e1e1e;display:flex;font-size:16px;font-weight:600;gap:8px;margin:0 0 16px}.formipay-notification-log svg{fill:#1e1e1e}.no-logs{color:#646970;font-size:13px;padding:20px 0;text-align:center}.notification-list{list-style:none;margin:0;padding:0}.notification-item{background:#f6f7f7;border:1px solid #e0e0e0;border-radius:4px;display:flex;gap:12px;margin-bottom:8px;padding:12px}.notification-icon{align-items:center;background:#fff;border-radius:50%;display:flex;flex-shrink:0;height:36px;justify-content:center;width:36px}.notification-icon svg{fill:#1e1e1e}.notification-content{flex:1;min-width:0}.notification-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:4px}.notification-type{color:#2271b1;font-size:10px;font-weight:700;text-transform:uppercase}.notification-status{border-radius:10px;font-size:10px;font-weight:600;padding:2px 6px}.notification-item.sent .notification-status{background:#e7f7ed;color:#28a745}.notification-item.failed .notification-status{background:#fbeaea;color:#dc3545}.notification-item.pending .notification-status{background:#fff8e5;color:#f0ad4e}.notification-details{display:flex;flex-direction:column;gap:2px}.notification-details strong{color:#1e1e1e;font-size:13px}.notification-date,.notification-recipient{color:#646970;font-size:11px}.notification-date{margin-top:4px}
.formipay-order-detail{background:#f6f7f7;display:flex;flex-direction:column;height:100%}.formipay-detail-header{align-items:center;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;gap:16px;justify-content:space-between;padding:16px 20px}.formipay-detail-header h1{flex:1;font-size:20px;font-weight:600;margin:0}.header-actions{display:flex;gap:8px}.formipay-detail-content{display:grid;gap:20px;grid-template-columns:2fr 1fr;overflow-y:auto;padding:20px}.formipay-detail-main,.formipay-detail-sidebar{display:flex;flex-direction:column;gap:20px}.formipay-detail-card{background:#fff;border:1px solid #e0e0e0;border-radius:4px;padding:20px}.formipay-detail-card h3{color:#1e1e1e;font-size:16px;font-weight:600;margin:0 0 16px}.detail-list{display:grid;gap:16px;grid-template-columns:repeat(2,1fr)}.detail-list>div{display:flex;flex-direction:column}.detail-list dt{color:#646970;font-size:12px;font-weight:600;margin-bottom:4px}.detail-list dd{align-items:center;color:#1e1e1e;display:flex;font-size:14px;gap:8px}.detail-list dd .components-select-control{flex:1}.items-table{border-collapse:collapse;width:100%}.items-table td,.items-table th{border-bottom:1px solid #f0f0f1;padding:10px;text-align:right}.items-table th{color:#646970;font-size:12px;font-weight:600;text-transform:uppercase}.items-table td{font-size:13px}.items-table small{color:#646970;display:block;font-size:11px}.items-table tfoot td{border-bottom:none;border-top:2px solid #1e1e1e;padding-top:16px}.customer-info{display:grid;gap:12px;grid-template-columns:1fr}.customer-info>div{display:flex;flex-direction:column}.customer-info dt{color:#646970;font-size:11px;font-weight:600;margin-bottom:2px}.customer-info dd{color:#1e1e1e;font-size:13px}.no-data{color:#646970;font-size:13px}.formipay-error,.formipay-loading{align-items:center;display:flex;flex-direction:column;gap:16px;justify-content:center;padding:60px 20px}.formipay-error p{color:#646970;margin:0}
.formipay-data-table-empty,.formipay-data-table-loading{padding:40px;text-align:center}.formipay-data-table{margin-top:20px}.formipay-data-table thead th{font-weight:600;padding:12px 10px}.formipay-data-table tbody td{padding:10px}.formipay-data-table tbody tr.is-clickable{cursor:pointer}.formipay-data-table tbody tr.is-clickable:hover{background-color:#f0f0f1}
.formipay-data-table-wrapper{margin:20px 0}.formipay-table-toolbar{align-items:center;display:flex;flex-wrap:wrap;gap:12px;margin-bottom:16px}.formipay-table-search{flex-grow:1;max-width:300px}.formipay-table-toolbar .components-select-control{min-width:150px}.formipay-filter-tabs{border-bottom:1px solid #ddd;display:flex;gap:4px;margin-bottom:16px}.formipay-filter-tabs .filter-tab{background:transparent;border:none;border-bottom:3px solid transparent;color:#646970;cursor:pointer;font-size:13px;padding:8px 16px;transition:all .2s}.formipay-filter-tabs .filter-tab:hover{background:#f0f0f1;color:#135e96}.formipay-filter-tabs .filter-tab.active{border-bottom-color:#135e96;color:#135e96;font-weight:600}.formipay-filter-tabs .filter-tab .count{background:#dcdcde;border-radius:10px;display:inline-block;font-size:11px;line-height:1.4;margin-right:6px;min-width:18px;padding:2px 6px}.formipay-filter-tabs .filter-tab.active .count{background:#135e96;color:#fff}.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{color:#646970;padding:40px;text-align:center}.formipay-table{border-collapse:collapse;width:100%}.formipay-table thead th{background:#f6f7f7;border-bottom:1px solid #c3c4c7;font-weight:600;padding:12px 10px;text-align:right}.formipay-table tbody td{border-bottom:1px solid #c3c4c7;padding:10px}.formipay-table tbody tr:last-child td{border-bottom:none}.formipay-table tbody tr:hover{background-color:#f0f0f1}.formipay-table .column-select{text-align:center;width:40px}.formipay-table tbody td:first-child input[type=checkbox]{margin:0}.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 .button-link,.formipay-table .row-actions a{color:#a7aaad;cursor:pointer;text-decoration:none}.formipay-table .row-actions .button-link:hover,.formipay-table .row-actions a:hover{color:#135e96}.formipay-table .row-actions .delete{color:#b32d2e}.formipay-table .row-actions .delete:hover{color:#d63638}.formipay-table .status-label{border-radius:4px;display:inline-block;font-size:12px;font-weight:500;padding:4px 8px}.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}.formipay-table input.formipay-form-shortcode{background:#f6f7f7;border:1px solid #8c8f94;border-radius:4px;color:#646970;font-family:monospace;font-size:12px;min-width:150px;padding:4px 8px}.formipay-table button.copy-shortcode{align-items:center;background:#fff;border:1px solid #8c8f94;border-radius:4px;cursor:pointer;display:inline-flex;gap:4px;margin-right:4px;padding:4px 8px}.formipay-table button.copy-shortcode:hover{background:#f6f7f7}.formipay-table button.copy-shortcode svg{height:16px;width:16px}.formipay-table-pagination{align-items:center;background:#fff;border:1px solid #c3c4c7;border-top:none;display:flex;justify-content:space-between;padding:12px 16px}.formipay-table-pagination .pagination-info{color:#646970;font-size:13px}.formipay-table-pagination .pagination-controls{align-items:center;display:flex;gap:8px}.formipay-table-pagination .page-info{color:#646970;font-size:13px;padding:0 8px}.formipay-table-pagination .components-select-control{min-width:80px}.formipay-modal-actions{display:flex;gap:12px;justify-content:flex-end;margin-top:20px}.formipay-table thead th.sorted{padding-left:20px;position:relative}.formipay-table thead th .sort-indicator{color:#135e96;position:absolute;left:8px;top:50%;transform:translateY(-50%)}
.formipay-page-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:20px}.formipay-page-header h1{font-size:23px;font-weight:400;margin:0}.status-badge{border-radius:4px;display:inline-block;font-size:12px;font-weight:500;padding:4px 8px}.status-badge.status-active,.status-badge.status-publish{background-color:#edfaef;color:#00a32a}.status-badge.status-draft,.status-badge.status-inactive{background-color:#f0f0f1;color:#646970}.status-badge.status-expired{background-color:#f6f7f7;color:#d63638}code{background-color:#f0f0f1;border-radius:3px;font-family:monospace;font-size:13px;padding:2px 6px}
.formipay-variation-table{border-collapse:collapse;margin-top:20px;width:100%}.formipay-variation-table thead th{border-bottom:2px solid #1e1e1e;color:#1e1e1e;font-size:13px;font-weight:600;padding:12px;text-align:right}.formipay-variation-table tbody td{border-bottom:1px solid #f0f0f1;padding:12px}.variation-row{background:#fff}.variation-name{gap:8px}.toggle-expand,.variation-name{align-items:center;display:flex}.toggle-expand{background:transparent;border:none;border-radius:2px;cursor:pointer;height:24px;justify-content:center;padding:0;width:24px}.toggle-expand:hover{background:#f0f0f1}.toggle-expand svg{fill:#646970}.variation-name strong{color:#1e1e1e;font-size:13px}.price-cell,.variation-stock,.variation-weight{min-width:120px}.price-cell input,.variation-stock input,.variation-weight input{border:1px solid #8c8f94;border-radius:2px;font-size:13px;padding:6px 8px;width:100%}.variation-stock .components-base-control,.variation-weight .components-base-control{margin:0}.variation-actions{min-width:100px}.variation-details-row{background:#f9f9f9}.variation-details-row td{padding:0}.inner-table{border-collapse:collapse;margin:0;width:100%}.inner-table thead{display:none}.inner-table td{border-bottom:1px solid #f0f0f1;padding:8px 12px}.inner-table tr:last-child td{border-bottom:none}.inner-table input[type=number]{border:1px solid #8c8f94;border-radius:2px;font-size:12px;padding:6px 8px;width:100%}.currency-name{color:#1e1e1e;font-size:12px;font-weight:600}.required{color:#dc3545;margin-right:4px}

View File

@@ -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/eye-closed', 'wp-icons/build/eye-opened', 'wp-icons/build/list', 'wp-icons/build/message', 'wp-icons/build/minus', 'wp-icons/build/plus', 'wp-icons/build/trash', 'wp-icons/build/visible'), 'version' => 'a62306d2a4d673c06ceb');
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/eye-closed', 'wp-icons/build/eye-opened', 'wp-icons/build/list', 'wp-icons/build/message', 'wp-icons/build/minus', 'wp-icons/build/plus', 'wp-icons/build/trash', 'wp-icons/build/visible'), 'version' => '164b137f81dde0451242');

View File

@@ -3,6 +3,6 @@
.formipay-order-timeline h3{color:#1e1e1e;font-size:16px;font-weight:600;margin:0 0 16px}.timeline-progress{justify-content:space-between;margin-bottom:24px}.timeline-progress,.timeline-step{align-items:center;display:flex;position:relative}.timeline-step{flex:1;flex-direction:column;gap:8px;z-index:1}.timeline-dot{background:#e0e0e0;border:2px solid #c3c4c7;border-radius:50%;height:24px;position:relative;width:24px}.timeline-step.completed .timeline-dot{background:#2271b1;border-color:#2271b1}.timeline-step.completed .timeline-dot:after{background:#fff;border-radius:50%;content:"";height:8px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:8px}.timeline-line{background:#e0e0e0;height:2px;left:50%;position:absolute;top:12px;width:100%;z-index:-1}.timeline-step.completed .timeline-line{background:#2271b1}.timeline-label{color:#646970;font-size:10px;font-weight:600;text-align:center;text-transform:uppercase}.timeline-events ul{list-style:none;margin:0;padding:0}.timeline-events li{border-bottom:1px solid #f0f0f1;display:grid;gap:8px;grid-template-columns:1fr auto;padding:12px 0}.timeline-events li:last-child{border-bottom:none}.event-status{color:#1e1e1e;font-size:13px;font-weight:600}.event-date{color:#646970;font-size:11px;text-align:right}.event-note{color:#646970;font-size:12px;grid-column:1/-1;margin-top:4px}.no-events{color:#646970;font-size:13px;padding:20px 0;text-align:center}
.formipay-notification-log h3{align-items:center;color:#1e1e1e;display:flex;font-size:16px;font-weight:600;gap:8px;margin:0 0 16px}.formipay-notification-log svg{fill:#1e1e1e}.no-logs{color:#646970;font-size:13px;padding:20px 0;text-align:center}.notification-list{list-style:none;margin:0;padding:0}.notification-item{background:#f6f7f7;border:1px solid #e0e0e0;border-radius:4px;display:flex;gap:12px;margin-bottom:8px;padding:12px}.notification-icon{align-items:center;background:#fff;border-radius:50%;display:flex;flex-shrink:0;height:36px;justify-content:center;width:36px}.notification-icon svg{fill:#1e1e1e}.notification-content{flex:1;min-width:0}.notification-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:4px}.notification-type{color:#2271b1;font-size:10px;font-weight:700;text-transform:uppercase}.notification-status{border-radius:10px;font-size:10px;font-weight:600;padding:2px 6px}.notification-item.sent .notification-status{background:#e7f7ed;color:#28a745}.notification-item.failed .notification-status{background:#fbeaea;color:#dc3545}.notification-item.pending .notification-status{background:#fff8e5;color:#f0ad4e}.notification-details{display:flex;flex-direction:column;gap:2px}.notification-details strong{color:#1e1e1e;font-size:13px}.notification-date,.notification-recipient{color:#646970;font-size:11px}.notification-date{margin-top:4px}
.formipay-order-detail{background:#f6f7f7;display:flex;flex-direction:column;height:100%}.formipay-detail-header{align-items:center;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;gap:16px;justify-content:space-between;padding:16px 20px}.formipay-detail-header h1{flex:1;font-size:20px;font-weight:600;margin:0}.header-actions{display:flex;gap:8px}.formipay-detail-content{display:grid;gap:20px;grid-template-columns:2fr 1fr;overflow-y:auto;padding:20px}.formipay-detail-main,.formipay-detail-sidebar{display:flex;flex-direction:column;gap:20px}.formipay-detail-card{background:#fff;border:1px solid #e0e0e0;border-radius:4px;padding:20px}.formipay-detail-card h3{color:#1e1e1e;font-size:16px;font-weight:600;margin:0 0 16px}.detail-list{display:grid;gap:16px;grid-template-columns:repeat(2,1fr)}.detail-list>div{display:flex;flex-direction:column}.detail-list dt{color:#646970;font-size:12px;font-weight:600;margin-bottom:4px}.detail-list dd{align-items:center;color:#1e1e1e;display:flex;font-size:14px;gap:8px}.detail-list dd .components-select-control{flex:1}.items-table{border-collapse:collapse;width:100%}.items-table td,.items-table th{border-bottom:1px solid #f0f0f1;padding:10px;text-align:left}.items-table th{color:#646970;font-size:12px;font-weight:600;text-transform:uppercase}.items-table td{font-size:13px}.items-table small{color:#646970;display:block;font-size:11px}.items-table tfoot td{border-bottom:none;border-top:2px solid #1e1e1e;padding-top:16px}.customer-info{display:grid;gap:12px;grid-template-columns:1fr}.customer-info>div{display:flex;flex-direction:column}.customer-info dt{color:#646970;font-size:11px;font-weight:600;margin-bottom:2px}.customer-info dd{color:#1e1e1e;font-size:13px}.no-data{color:#646970;font-size:13px}.formipay-error,.formipay-loading{align-items:center;display:flex;flex-direction:column;gap:16px;justify-content:center;padding:60px 20px}.formipay-error p{color:#646970;margin:0}
.formipay-data-table-empty,.formipay-data-table-loading{padding:40px;text-align:center}.formipay-data-table{margin-top:20px}.formipay-data-table thead th{font-weight:600;padding:12px 10px}.formipay-data-table tbody td{padding:10px}.formipay-data-table tbody tr.is-clickable{cursor:pointer}.formipay-data-table tbody tr.is-clickable:hover{background-color:#f0f0f1}
.formipay-data-table-wrapper{margin:20px 0}.formipay-table-toolbar{align-items:center;display:flex;flex-wrap:wrap;gap:12px;margin-bottom:16px}.formipay-table-search{flex-grow:1;max-width:300px}.formipay-table-toolbar .components-select-control{min-width:150px}.formipay-filter-tabs{border-bottom:1px solid #ddd;display:flex;gap:4px;margin-bottom:16px}.formipay-filter-tabs .filter-tab{background:transparent;border:none;border-bottom:3px solid transparent;color:#646970;cursor:pointer;font-size:13px;padding:8px 16px;transition:all .2s}.formipay-filter-tabs .filter-tab:hover{background:#f0f0f1;color:#135e96}.formipay-filter-tabs .filter-tab.active{border-bottom-color:#135e96;color:#135e96;font-weight:600}.formipay-filter-tabs .filter-tab .count{background:#dcdcde;border-radius:10px;display:inline-block;font-size:11px;line-height:1.4;margin-left:6px;min-width:18px;padding:2px 6px}.formipay-filter-tabs .filter-tab.active .count{background:#135e96;color:#fff}.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{color:#646970;padding:40px;text-align:center}.formipay-table{border-collapse:collapse;width:100%}.formipay-table thead th{background:#f6f7f7;border-bottom:1px solid #c3c4c7;font-weight:600;padding:12px 10px;text-align:left}.formipay-table tbody td{border-bottom:1px solid #c3c4c7;padding:10px}.formipay-table tbody tr:last-child td{border-bottom:none}.formipay-table tbody tr:hover{background-color:#f0f0f1}.formipay-table .column-select{text-align:center;width:40px}.formipay-table tbody td:first-child input[type=checkbox]{margin:0}.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 .button-link,.formipay-table .row-actions a{color:#a7aaad;cursor:pointer;text-decoration:none}.formipay-table .row-actions .button-link:hover,.formipay-table .row-actions a:hover{color:#135e96}.formipay-table .row-actions .delete{color:#b32d2e}.formipay-table .row-actions .delete:hover{color:#d63638}.formipay-table .status-label{border-radius:4px;display:inline-block;font-size:12px;font-weight:500;padding:4px 8px}.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}.formipay-table input.formipay-form-shortcode{background:#f6f7f7;border:1px solid #8c8f94;border-radius:4px;color:#646970;font-family:monospace;font-size:12px;min-width:150px;padding:4px 8px}.formipay-table button.copy-shortcode{align-items:center;background:#fff;border:1px solid #8c8f94;border-radius:4px;cursor:pointer;display:inline-flex;gap:4px;margin-left:4px;padding:4px 8px}.formipay-table button.copy-shortcode:hover{background:#f6f7f7}.formipay-table button.copy-shortcode svg{height:16px;width:16px}.formipay-table-pagination{align-items:center;background:#fff;border:1px solid #c3c4c7;border-top:none;display:flex;justify-content:space-between;padding:12px 16px}.formipay-table-pagination .pagination-info{color:#646970;font-size:13px}.formipay-table-pagination .pagination-controls{align-items:center;display:flex;gap:8px}.formipay-table-pagination .page-info{color:#646970;font-size:13px;padding:0 8px}.formipay-table-pagination .components-select-control{min-width:80px}.formipay-modal-actions{display:flex;gap:12px;justify-content:flex-end;margin-top:20px}.formipay-table thead th.sorted{padding-right:20px;position:relative}.formipay-table thead th .sort-indicator{color:#135e96;position:absolute;right:8px;top:50%;transform:translateY(-50%)}
.formipay-page-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:20px}.formipay-page-header h1{font-size:23px;font-weight:400;margin:0}.status-badge{border-radius:4px;display:inline-block;font-size:12px;font-weight:500;padding:4px 8px}.status-badge.status-active,.status-badge.status-publish{background-color:#edfaef;color:#00a32a}.status-badge.status-draft,.status-badge.status-inactive{background-color:#f0f0f1;color:#646970}.status-badge.status-expired{background-color:#f6f7f7;color:#d63638}code{background-color:#f0f0f1;border-radius:3px;font-family:monospace;font-size:13px;padding:2px 6px}
.formipay-variation-table{border-collapse:collapse;margin-top:20px;width:100%}.formipay-variation-table thead th{border-bottom:2px solid #1e1e1e;color:#1e1e1e;font-size:13px;font-weight:600;padding:12px;text-align:left}.formipay-variation-table tbody td{border-bottom:1px solid #f0f0f1;padding:12px}.variation-row{background:#fff}.variation-name{gap:8px}.toggle-expand,.variation-name{align-items:center;display:flex}.toggle-expand{background:transparent;border:none;border-radius:2px;cursor:pointer;height:24px;justify-content:center;padding:0;width:24px}.toggle-expand:hover{background:#f0f0f1}.toggle-expand svg{fill:#646970}.variation-name strong{color:#1e1e1e;font-size:13px}.price-cell,.variation-stock,.variation-weight{min-width:120px}.price-cell input,.variation-stock input,.variation-weight input{border:1px solid #8c8f94;border-radius:2px;font-size:13px;padding:6px 8px;width:100%}.variation-stock .components-base-control,.variation-weight .components-base-control{margin:0}.variation-actions{min-width:100px}.variation-details-row{background:#f9f9f9}.variation-details-row td{padding:0}.inner-table{border-collapse:collapse;margin:0;width:100%}.inner-table thead{display:none}.inner-table td{border-bottom:1px solid #f0f0f1;padding:8px 12px}.inner-table tr:last-child td{border-bottom:none}.inner-table input[type=number]{border:1px solid #8c8f94;border-radius:2px;font-size:12px;padding:6px 8px;width:100%}.currency-name{color:#1e1e1e;font-size:12px;font-weight:600}.required{color:#dc3545;margin-left:4px}

File diff suppressed because one or more lines are too long

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>