vasuzex 2.3.13 → 2.3.15

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,135 @@
2
2
 
3
3
  All notable changes to Vasuzex will be documented in this file.
4
4
 
5
+ ## [2.3.15] - 2026-04-20
6
+
7
+ ### ✨ Added
8
+
9
+ #### DataTable — Mobile Card View (`vasuzex/react`)
10
+
11
+ - **New `mobileCardView` prop** — When `mobileCardView={true}`, DataTable renders `MobileCardList` instead of the standard table on viewports narrower than 640px (Tailwind `sm` breakpoint)
12
+ - **`MobileCardList` component** — Stacked card layout optimised for touch
13
+ - Column priority system: mark columns as `priority="primary"` (shown full-width, no label), `priority="secondary"` (compact label:value rows), or omit to auto-assign first 3 as primary and rest as secondary
14
+ - Supports full action set: view, edit, delete, hard-delete (🔥), and restore actions
15
+ - Skeleton loading state with `animate-pulse` placeholders
16
+ - Empty-state text support via `emptyText` prop
17
+ - **New `useMobileDetect` hook** — `window.matchMedia`-based viewport detection; no resize listener polling, reacts instantly, SSR-safe fallback
18
+
19
+ #### Pagination — Mobile Responsive Layout (`vasuzex/react`)
20
+
21
+ - **Mobile compact mode** (visible on `sm:hidden`): Prev / "Page X of Y" + jump-to-page input / Next
22
+ - Jump-to-page input: type a page number and press Enter (or blur) to navigate directly
23
+ - **Desktop unchanged**: full numbered page buttons with ellipsis remain as before
24
+
25
+ #### BreadCrumb — Responsive Props (`vasuzex/react`)
26
+
27
+ - Added `className` prop for external style overrides
28
+ - Responsive typography and spacing: `text-lg sm:text-xl`, `gap-2 sm:gap-3`, `mb-3 sm:mb-6`
29
+
30
+ ### 🐛 Fixed
31
+
32
+ #### Filters — Responsive Layout (`vasuzex/react`)
33
+
34
+ - Moved refresh button inline with rows-per-page selector and showing info (no longer isolated in a separate row)
35
+ - Shortened label: "Rows:" instead of "Rows per page:" — saves horizontal space on small screens
36
+ - Compact showing info format: `1–10 of 50` instead of `Showing 1 to 10 of 50 items`
37
+
38
+ #### RowActionsCell — Dropdown Clipping Fix (`vasuzex/react`)
39
+
40
+ - **Root cause**: Dropdown was positioned `absolute` inside the table cell, causing it to be clipped by `overflow-x-auto` on the table container
41
+ - **Fix**: Dropdown now uses `position: fixed` anchored to the trigger button via `getBoundingClientRect()`, with `z-index: 9999` — never clipped by any overflow parent
42
+ - Fixed click-outside detection: separate `btnRef` and `dropdownRef` so clicking the button or inside the menu never incorrectly closes the dropdown
43
+
44
+ ---
45
+
46
+ ## [2.3.14] - 2026-04-08
47
+
48
+ ### 🐛 Fixed
49
+
50
+ #### DataTable — Content Loader (Skeleton) on Sort/Search/Filter (`vasuzex/react`)
51
+
52
+ **Root Causes & Fixes:**
53
+
54
+ 1. **Browser Paint Timing Issue** — Skeleton loader not visible on sort/search (only on explicit refresh click)
55
+ - **Root cause**: React 18 `createRoot` + fast localhost API responses (2-10ms) meant the skeleton commit and data response both arrived within the same vsync frame (~16ms), so the browser only painted the final data state
56
+ - **Fix**: `useLayoutEffect` commits skeleton synchronously + `rAF → setTimeout(0)` defers XHR until AFTER browser paint
57
+ - **Technical flow**:
58
+ 1. `useLayoutEffect` → skeleton committed to DOM (synchronously, before paint)
59
+ 2. Passive effect queues `rAF` → scheduled pre-paint
60
+ 3. `rAF` queues `setTimeout(0)` → scheduled as macro-task (after paint)
61
+ 4. Browser **paints skeleton** ← now visible to user
62
+ 5. `setTimeout(0)` fires → `fetchData()` → XHR opens → response arrives after skeleton is on screen
63
+
64
+ 2. **API Client Error Interceptor Stripping Cancel Errors** — "No data found" briefly appeared on every sort/search
65
+ - **Root cause**: axios cancel errors have `err.code === 'ERR_CANCELED'` which DataTable checks — but error interceptor transformed ALL errors to plain `{ message, errors }` objects, stripping the code/name properties
66
+ - **Impact**: Cancelled requests hit the error path → `setData([])` + `setLoading(false)` → "No data found" flash, then data arrived
67
+ - **Fix**: Both api clients (`admin/web` and `business/web`) now check `if (axios.isCancel(error))` early and pass through untransformed
68
+
69
+ 3. **Double-Fetch on Every Interaction** — Multiple concurrent requests on sort/search
70
+ - **Root cause**: `refreshSignal` and `refreshKey` effects had `fetchData` in deps. `fetchData` recreates on every sort/search (because its own deps change). Since every admin page passes `refreshSignal={refreshKey}`, BOTH the main fetch effect AND the refreshSignal effect fired on every interaction → 2+ concurrent requests
71
+ - **Fix**: Introduced `fetchDataRef` — holds always-current `fetchData` without needing it in secondary effect deps. Secondary effects now only fire when their actual trigger (`refreshSignal` value or `refreshKey` value) changes
72
+
73
+ 4. **Tailwind CSS Classes Not Generated** — animate-pulse skeleton class missing from bundle
74
+ - **Root cause**: Admin web's `tailwind.config.js` only scanned `./src/**/*`, not the symlinked `vasuzex-v2` directory
75
+ - **Impact**: `animate-pulse` utility class not in admin web's CSS bundle (business web already had correct path)
76
+ - **Fix**: Added `../../../vasuzex-v2/frontend/react-ui/**/*.{js,jsx}` to Tailwind content array (matching business web pattern)
77
+
78
+ **Files Modified:**
79
+ - [`DataTable.jsx`](frontend/react-ui/components/DataTable/DataTable.jsx) — useLayoutEffect + rAF+setTimeout pattern + fetchDataRef
80
+ - `apps/admin/web/src/lib/api-client.js` — axios.isCancel check before error transformation
81
+ - `apps/business/web/src/lib/apiClient.js` — axios.isCancel check before error transformation
82
+ - `apps/admin/web/tailwind.config.js` — added vasuzex-v2 path to content array
83
+
84
+ **Result:** Skeleton loader now reliably shows on sort, search, filter, and every other data trigger, then data smoothly renders when ready. No "no data found" flash.
85
+
86
+ #### Model — Soft-Delete Restore Fix (`vasuzex/eloquent`)
87
+
88
+ - **Issue**: `restore()` method used `save()` which triggered model observers and allowed normal query scopes, causing inconsistent state
89
+ - **Fix**: Direct database update via `withTrashed()` query builder to bypass soft-delete scope
90
+ - **Changes**:
91
+ - Use `withTrashed().where(pk, id).update()` instead of `save()`
92
+ - Auto-update `updated_at` timestamp when timestamps enabled
93
+ - Calls `syncOriginal()` to update model cache state
94
+ - Fires `restored` model event after DB update completes
95
+ - **Impact**: Soft-deleted records now restore cleanly without triggering update observers
96
+
97
+ #### MediaManager — WebP/AVIF Format Negotiation (`vasuzex/services`)
98
+
99
+ - **Issue**: Media serving didn't support modern image formats (WebP, AVIF) or client content-type negotiation
100
+ - **Enhancements**:
101
+ 1. **Format negotiation** — Query param `?format=webp|avif|jpeg|png` overrides client Accept header
102
+ 2. **LRU in-memory cache** — Hot thumbnails cached in memory (200 entry limit) to avoid repeated filesystem hits
103
+ 3. **Format-aware disk cache keys** — WebP and JPEG of same image cached separately
104
+ 4. **ETag support** — Content MD5 hash for conditional requests (304 Not Modified)
105
+ 5. **Immutable cache headers** — 1-year max-age via dedicated controller
106
+ 6. **Direct format lookup** — Cache lookup by format (no extension loop)
107
+ - **Performance**: 5-50x faster for repeated hot thumbnail requests
108
+ - **Files Modified**: [`framework/Services/Media/MediaManager.js`](framework/Services/Media/MediaManager.js)
109
+
110
+ ### ✨ Added
111
+
112
+ #### ActionDefaults — Hard Delete & Restore Actions (`vasuzex/react`)
113
+
114
+ - **Hard Delete Action** — Permanent delete with severe confirmation (Flame icon 🔥)
115
+ - Shows in trash-only mode for trashed records
116
+ - `DELETE ?hardDelete=true` query parameter
117
+ - `createHardDeleteClickHandler()` helper
118
+ - "Cannot be undone" warning in confirmation dialog
119
+
120
+ - **Restore Action** — Restore soft-deleted records (RotateCcw icon)
121
+ - Shows for trashed rows
122
+ - `PATCH {restoreUrl}` request
123
+ - `createRestoreClickHandler()` helper
124
+ - Smooth restore with toast notification
125
+
126
+ - **Custom Action Tooltip** — Auto-generate tooltip from label when title not provided
127
+ - Improves UX for custom actions without explicit title
128
+
129
+ **Files Modified:** [`frontend/react-ui/components/DataTable/ActionDefaults.jsx`](frontend/react-ui/components/DataTable/ActionDefaults.jsx)
130
+
131
+ **Impact:** Complete soft-delete/trash workflow now supported in DataTable — view, restore, or permanently delete with proper confirmations.
132
+
133
+
5
134
  ## [2.3.13] - 2026-04-05
6
135
 
7
136
  ### 🐛 Fixed
@@ -535,15 +535,28 @@ export class Model extends GuruORMModel {
535
535
  return false;
536
536
  }
537
537
 
538
- this.setAttribute(this.constructor.deletedAt, null);
538
+ const deletedAtColumn = this.constructor.deletedAt;
539
+ const pk = this.constructor.primaryKey || 'id';
540
+ const updates = { [deletedAtColumn]: null };
541
+
542
+ // Add updated_at if timestamps are enabled
543
+ if (this.constructor.timestamps && this.constructor.updatedAt) {
544
+ updates[this.constructor.updatedAt] = new Date();
545
+ }
539
546
 
540
- const result = await this.save();
547
+ // Use withTrashed() to bypass the soft-delete scope — a trashed record isn't
548
+ // visible to the normal query (which adds WHERE deleted_at IS NULL).
549
+ await this.constructor.withTrashed().where(pk, this.getKey()).update(updates);
541
550
 
542
- if (result) {
543
- await this.fireModelEvent('restored', false);
551
+ this.setAttribute(deletedAtColumn, null);
552
+ if (updates[this.constructor.updatedAt]) {
553
+ this.setAttribute(this.constructor.updatedAt, updates[this.constructor.updatedAt]);
544
554
  }
555
+ this.syncOriginal();
545
556
 
546
- return result;
557
+ await this.fireModelEvent('restored', false);
558
+
559
+ return true;
547
560
  }
548
561
 
549
562
  /**