vasuzex 2.3.5 → 2.3.7

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.
@@ -26,6 +26,7 @@ import { Link } from 'react-router-dom';
26
26
  * @param {Function} props.onReject - Reject action handler (approval workflows)
27
27
  * @param {Function} props.onViewHistory - View approval history handler
28
28
  * @param {boolean} props.hasApproval - Whether to show approval actions (default: false)
29
+ * @param {Array} props.customActions - Extra menu items: [{ label, icon, onClick, className, condition, separator }]
29
30
  * @returns {JSX.Element}
30
31
  */
31
32
  export function RowActionsCell({
@@ -39,6 +40,7 @@ export function RowActionsCell({
39
40
  onReject,
40
41
  onViewHistory,
41
42
  hasApproval = false,
43
+ customActions = [],
42
44
  }) {
43
45
  const [isMenuOpen, setIsMenuOpen] = useState(false);
44
46
  const menuRef = useRef(null);
@@ -157,6 +159,33 @@ export function RowActionsCell({
157
159
  </>
158
160
  )}
159
161
 
162
+ {/* Custom Actions */}
163
+ {customActions.length > 0 && (
164
+ <>
165
+ {customActions.map((action, idx) => {
166
+ if (action.condition && !action.condition(row)) return null;
167
+ return (
168
+ <React.Fragment key={idx}>
169
+ {action.separator && (
170
+ <div className="border-t border-gray-200 dark:border-gray-700 my-1" />
171
+ )}
172
+ <button
173
+ onClick={() => {
174
+ action.onClick(row);
175
+ setIsMenuOpen(false);
176
+ }}
177
+ className={`w-full flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-700 ${action.className || 'text-gray-700 dark:text-gray-300'}`}
178
+ >
179
+ {action.icon && <action.icon className="h-4 w-4 shrink-0" />}
180
+ <span>{action.label}</span>
181
+ </button>
182
+ </React.Fragment>
183
+ );
184
+ })}
185
+ <div className="border-t border-gray-200 dark:border-gray-700 my-1" />
186
+ </>
187
+ )}
188
+
160
189
  {/* Toggle Status */}
161
190
  {onToggle && (
162
191
  <button
@@ -210,6 +239,14 @@ RowActionsCell.propTypes = {
210
239
  onReject: PropTypes.func,
211
240
  onViewHistory: PropTypes.func,
212
241
  hasApproval: PropTypes.bool,
242
+ customActions: PropTypes.arrayOf(PropTypes.shape({
243
+ label: PropTypes.string.isRequired,
244
+ icon: PropTypes.elementType,
245
+ onClick: PropTypes.func.isRequired,
246
+ className: PropTypes.string,
247
+ condition: PropTypes.func,
248
+ separator: PropTypes.bool,
249
+ })),
213
250
  };
214
251
 
215
252
  export default RowActionsCell;
@@ -59,6 +59,10 @@ try {
59
59
  * @param {string} props.emptyText - Text to show when no data
60
60
  * @param {boolean} props.persistState - Enable URL state persistence (default: true)
61
61
  */
62
+
63
+ // URL params that DataTable owns — all other params (e.g. trashed) are preserved as-is
64
+ const DT_PARAMS = ['page', 'sortBy', 'sortOrder', 'search', 'statusFilter', 'limit'];
65
+
62
66
  export function DataTable(props) {
63
67
  // Internal refresh key for self-refresh
64
68
  const [refreshKey, setRefreshKey] = React.useState(0);
@@ -179,39 +183,43 @@ export function DataTable(props) {
179
183
  const updateURL = React.useCallback((state) => {
180
184
  if (!persistState || typeof window === 'undefined') return;
181
185
 
182
-
186
+ // Always read from window.location.search (current, live URL) not from
187
+ // the searchParams closure — the closure can be one render behind when
188
+ // the parent just called setSearchParams in the same batch.
189
+ const current = new URLSearchParams(window.location.search);
190
+
191
+ // Remove DataTable-managed params
192
+ DT_PARAMS.forEach(k => current.delete(k));
193
+ for (const key of [...current.keys()]) {
194
+ if (key.startsWith('columnSearch[')) current.delete(key);
195
+ }
196
+
197
+ // Re-add non-default values to keep URL clean
198
+ if (state.page !== 1) current.set('page', state.page);
199
+ if (state.sortBy) current.set('sortBy', state.sortBy);
200
+ if (state.sortOrder !== initialSortOrder) current.set('sortOrder', state.sortOrder);
201
+ if (state.search) current.set('search', state.search);
202
+ if (state.statusFilter !== 'all') current.set('statusFilter', state.statusFilter);
203
+ if (state.limit !== (initialLimit || 10)) current.set('limit', state.limit);
183
204
 
184
- const params = new URLSearchParams();
185
-
186
- // Only add non-default values to keep URL clean
187
- if (state.page !== 1) params.set('page', state.page);
188
- if (state.sortBy) params.set('sortBy', state.sortBy);
189
- if (state.sortOrder !== initialSortOrder) params.set('sortOrder', state.sortOrder);
190
- if (state.search) params.set('search', state.search);
191
- if (state.statusFilter !== 'all') params.set('statusFilter', state.statusFilter);
192
- if (state.limit !== (initialLimit || 10)) params.set('limit', state.limit);
193
-
194
205
  // Add column search params
195
206
  Object.entries(state.columnSearch || {}).forEach(([field, value]) => {
196
- if (value) params.set(`columnSearch[${field}]`, value);
207
+ if (value) current.set(`columnSearch[${field}]`, value);
197
208
  });
198
209
 
199
210
  // Use React Router if available, otherwise window.history
200
211
  if (hasReactRouter && setSearchParams) {
201
-
202
- setSearchParams(params, { replace: true });
212
+ setSearchParams(current, { replace: true });
203
213
  } else if (typeof window !== 'undefined') {
204
- const newURL = params.toString()
205
- ? `${window.location.pathname}?${params.toString()}`
214
+ const newURL = current.toString()
215
+ ? `${window.location.pathname}?${current.toString()}`
206
216
  : window.location.pathname;
207
-
208
-
209
217
  window.history.replaceState({}, '', newURL);
210
218
  }
211
- }, [persistState, hasReactRouter, setSearchParams]);
219
+ }, [persistState, hasReactRouter, setSearchParams, initialSortOrder, initialLimit]);
212
220
 
213
221
  /**
214
- * Sync URL whenever state changes
222
+ * Sync URL whenever DataTable state changes
215
223
  */
216
224
  React.useEffect(() => {
217
225
  updateURL({
@@ -77,6 +77,7 @@ export function EnhancedPhotoManager({
77
77
  deletePhoto,
78
78
  deleteMultiplePhotos,
79
79
  reorderPhotos,
80
+ setPrimaryPhoto,
80
81
  } = photosHook;
81
82
 
82
83
  const [selectedPhotos, setSelectedPhotos] = useState([]);
@@ -255,6 +256,7 @@ export function EnhancedPhotoManager({
255
256
  isSelected={selectedPhotos.includes(photo.id)}
256
257
  onSelect={handlePhotoSelect}
257
258
  onDelete={handlePhotoDelete}
259
+ onSetPrimary={setPrimaryPhoto || undefined}
258
260
  disabled={loading}
259
261
  />
260
262
  ))}
@@ -294,6 +296,7 @@ EnhancedPhotoManager.propTypes = {
294
296
  deletePhoto: PropTypes.func,
295
297
  deleteMultiplePhotos: PropTypes.func,
296
298
  reorderPhotos: PropTypes.func,
299
+ setPrimaryPhoto: PropTypes.func,
297
300
  }).isRequired,
298
301
  /** Section title */
299
302
  title: PropTypes.string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vasuzex",
3
- "version": "2.3.5",
3
+ "version": "2.3.7",
4
4
  "description": "Laravel-inspired framework for Node.js monorepos - V2 with optimized dependencies",
5
5
  "type": "module",
6
6
  "main": "./framework/index.js",