vasuzex 2.3.12 → 2.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,15 +5,19 @@ import {
5
5
  applyActionDefaults,
6
6
  createViewClickHandler,
7
7
  createDeleteClickHandler,
8
+ createHardDeleteClickHandler,
9
+ createRestoreClickHandler,
10
+ ACTION_DEFAULTS,
8
11
  } from "./ActionDefaults.jsx";
9
12
 
10
13
  /**
11
14
  * TableBody Component - Production Ready
12
- *
13
- * Table body with data rows, column rendering, and action buttons
14
- * Handles Switch component for status toggle
15
- * Auto-configures edit/view/delete actions
16
- *
15
+ *
16
+ * Table body with data rows, column rendering, and action buttons.
17
+ * Handles Switch component for status toggle.
18
+ * Trash-aware: when trashMode='only', delete becomes hardDelete with warning;
19
+ * restore button shown automatically for trashed rows.
20
+ *
17
21
  * @module components/DataTable/TableBody
18
22
  */
19
23
  export function TableBody({
@@ -27,14 +31,26 @@ export function TableBody({
27
31
  resourceName,
28
32
  resourceIdField = "id",
29
33
  onRefresh,
34
+ // Trash support: 'without' | 'with' | 'only'
35
+ trashMode,
36
+ // URL for the restore endpoint (e.g. "/products/:id/restore")
37
+ restoreUrl,
30
38
  }) {
31
39
  if (loading) {
40
+ const skeletonRows = Array.from({ length: 5 });
41
+ const colCount = columns.length + (actions ? 1 : 0);
32
42
  return (
33
- <tr>
34
- <td colSpan={columns.length + (actions ? 1 : 0)} className="text-center py-8">
35
- Loading data...
36
- </td>
37
- </tr>
43
+ <>
44
+ {skeletonRows.map((_, rowIdx) => (
45
+ <tr key={rowIdx} className="border-b border-gray-200 dark:border-gray-700 animate-pulse">
46
+ {Array.from({ length: colCount }).map((_, colIdx) => (
47
+ <td key={colIdx} className="px-6 py-4">
48
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
49
+ </td>
50
+ ))}
51
+ </tr>
52
+ ))}
53
+ </>
38
54
  );
39
55
  }
40
56
 
@@ -83,6 +99,11 @@ export function TableBody({
83
99
  return null;
84
100
  }
85
101
 
102
+ // In trash-only mode: skip edit/switch/status-toggle actions
103
+ if (trashMode === 'only' && ['edit', 'switch'].includes(userAction.name)) {
104
+ return null;
105
+ }
106
+
86
107
  // Apply defaults based on action name
87
108
  const action = applyActionDefaults(userAction, resourceName, resourceIdField);
88
109
 
@@ -114,22 +135,46 @@ export function TableBody({
114
135
  }
115
136
  }
116
137
 
117
- // Handle delete action with automatic confirmation
138
+ // Handle delete action:
139
+ // — in trash-only mode, OR when row is already soft-deleted (has deleted_at) → hard-delete with permanent-delete confirmation
140
+ // — normal mode → soft-delete with standard confirmation
118
141
  if (action.type === "button" && action.name === "delete") {
119
142
  const deleteAction = action;
120
143
  if (deleteAction.deleteUrl && !userAction.onClick) {
121
- deleteAction.onClick = createDeleteClickHandler(
122
- api,
123
- deleteAction.deleteUrl,
124
- deleteAction.confirmMessage || "Are you sure you want to delete this item?",
125
- resourceIdField,
126
- {
127
- confirmTitle: deleteAction.confirmTitle,
128
- confirmButtonText: deleteAction.confirmButtonText,
129
- successMessage: deleteAction.successMessage,
130
- onRefresh,
131
- },
132
- );
144
+ const isAlreadyTrashed = !!row.deleted_at;
145
+ if (trashMode === 'only' || isAlreadyTrashed) {
146
+ // Override to hardDelete
147
+ deleteAction.onClick = createHardDeleteClickHandler(
148
+ api,
149
+ deleteAction.deleteUrl,
150
+ deleteAction.confirmMessage || "This will permanently remove the record from the database. This action cannot be undone.",
151
+ resourceIdField,
152
+ {
153
+ confirmTitle: "Permanently Delete?",
154
+ confirmButtonText: "Yes, permanently delete!",
155
+ successMessage: deleteAction.successMessage || "Permanently deleted",
156
+ onRefresh,
157
+ },
158
+ );
159
+ // Apply hardDelete styling
160
+ if (!userAction.className) {
161
+ deleteAction.className = ACTION_DEFAULTS.hardDelete.extraClass;
162
+ }
163
+ deleteAction.title = "Permanently Delete";
164
+ } else {
165
+ deleteAction.onClick = createDeleteClickHandler(
166
+ api,
167
+ deleteAction.deleteUrl,
168
+ deleteAction.confirmMessage || "Are you sure you want to delete this item?",
169
+ resourceIdField,
170
+ {
171
+ confirmTitle: deleteAction.confirmTitle,
172
+ confirmButtonText: deleteAction.confirmButtonText,
173
+ successMessage: deleteAction.successMessage,
174
+ onRefresh,
175
+ },
176
+ );
177
+ }
133
178
  }
134
179
  }
135
180
 
@@ -138,7 +183,7 @@ export function TableBody({
138
183
  const className =
139
184
  typeof action.className === "function"
140
185
  ? action.className(row)
141
- : action.className || "";
186
+ : action.className || action.extraClass || "";
142
187
  const title =
143
188
  typeof action.title === "function"
144
189
  ? action.title(row)
@@ -183,6 +228,22 @@ export function TableBody({
183
228
 
184
229
  return null;
185
230
  })}
231
+
232
+ {/* Auto-inject Restore button when trashMode is active and row is trashed */}
233
+ {restoreUrl && (trashMode === 'only' || (trashMode === 'with' && row.deleted_at)) && (() => {
234
+ const handler = createRestoreClickHandler(api, restoreUrl, resourceIdField, { onRefresh });
235
+ const Icon = ACTION_DEFAULTS.restore.icon;
236
+ return (
237
+ <button
238
+ key="auto-restore"
239
+ onClick={() => handler(row)}
240
+ className={ACTION_DEFAULTS.restore.extraClass}
241
+ title="Restore"
242
+ >
243
+ <Icon className="h-4 w-4" />
244
+ </button>
245
+ );
246
+ })()}
186
247
  </div>
187
248
  </td>
188
249
  )}
@@ -1,23 +1,52 @@
1
1
  import React from "react";
2
2
 
3
+ /**
4
+ * Skeleton pulse block – light-grey bar that animates.
5
+ * Width can be "full", "3/4", "1/2", "1/3" or a fixed px value.
6
+ */
7
+ const SkeletonCell = ({ width = "3/4", height = "h-4" }) => {
8
+ const widthClass =
9
+ width === "full" ? "w-full" :
10
+ width === "3/4" ? "w-3/4" :
11
+ width === "1/2" ? "w-1/2" :
12
+ width === "1/3" ? "w-1/3" :
13
+ width === "1/4" ? "w-1/4" : width;
14
+
15
+ return (
16
+ <div
17
+ className={`${height} ${widthClass} rounded bg-gray-200 dark:bg-gray-700 animate-pulse`}
18
+ />
19
+ );
20
+ };
21
+
22
+ /**
23
+ * A single skeleton row matching the number of visible columns + optional action column.
24
+ * Alternates bar widths so the shimmer does not look monotonous.
25
+ */
26
+ const WIDTHS = ["3/4", "1/2", "full", "1/3", "3/4", "1/2"];
27
+
28
+ const SkeletonRow = ({ colSpan }) => (
29
+ <tr className="border-b border-gray-200 dark:border-gray-700">
30
+ {Array.from({ length: colSpan }).map((_, i) => (
31
+ <td key={i} className="px-6 py-4">
32
+ <SkeletonCell width={WIDTHS[i % WIDTHS.length]} />
33
+ </td>
34
+ ))}
35
+ </tr>
36
+ );
37
+
3
38
  /**
4
39
  * TableState Component
5
- * Handles loading and empty states for DataTable
40
+ * Handles loading (skeleton rows) and empty states for DataTable
6
41
  */
7
- export const TableState = ({ loading, empty, colSpan, emptyText }) => {
42
+ export const TableState = ({ loading, empty, colSpan, emptyText, skeletonRows = 6 }) => {
8
43
  if (loading) {
9
44
  return (
10
- <tr>
11
- <td colSpan={colSpan} className="text-center py-8">
12
- <div className="flex items-center justify-center gap-2">
13
- <svg className="animate-spin h-5 w-5 text-brand-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
14
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
15
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
16
- </svg>
17
- <span>Loading data...</span>
18
- </div>
19
- </td>
20
- </tr>
45
+ <>
46
+ {Array.from({ length: skeletonRows }).map((_, i) => (
47
+ <SkeletonRow key={i} colSpan={colSpan} />
48
+ ))}
49
+ </>
21
50
  );
22
51
  }
23
52
  if (empty) {
package/jsconfig.json CHANGED
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "module": "esnext",
16
16
  "moduleResolution": "node",
17
+ "ignoreDeprecations": "6.0",
17
18
  "target": "es2020",
18
19
  "lib": ["es2020"],
19
20
  "allowSyntheticDefaultImports": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vasuzex",
3
- "version": "2.3.12",
3
+ "version": "2.3.14",
4
4
  "description": "Laravel-inspired framework for Node.js monorepos - V2 with optimized dependencies",
5
5
  "type": "module",
6
6
  "main": "./framework/index.js",