vasuzex 2.3.11 → 2.3.13
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 +29 -0
- package/README.md +505 -514
- package/framework/Database/Model.js +12 -4
- package/frontend/react-ui/components/DataTable/DataTable.jsx +48 -7
- package/jsconfig.json +1 -0
- package/package.json +2 -2
|
@@ -859,14 +859,22 @@ export class Model extends GuruORMModel {
|
|
|
859
859
|
if (this.timestamps && this.updatedAt) {
|
|
860
860
|
if (!Object.prototype.hasOwnProperty.call(this, '_cachedUpdateFn')) {
|
|
861
861
|
const modelClass = this;
|
|
862
|
-
// query.update
|
|
863
|
-
//
|
|
864
|
-
|
|
862
|
+
// IMPORTANT: Do NOT capture `query.update` here. `query` is a Proxy, and
|
|
863
|
+
// accessing `.update` on a Proxy returns a new wrapper closure that hardcodes
|
|
864
|
+
// the specific builder instance (`target`) from that call. Caching that wrapper
|
|
865
|
+
// means all subsequent `.update()` calls would execute against the ORIGINAL
|
|
866
|
+
// builder's QueryBuilder (from the first call), ignoring the current query's
|
|
867
|
+
// WHERE clauses (e.g. the per-record `WHERE id = ?` clause would be lost).
|
|
868
|
+
//
|
|
869
|
+
// Instead, use `this.query.update(data)` directly. When _cachedUpdateFn is
|
|
870
|
+
// invoked, `this` is the current EloquentBuilder instance (set by the Proxy's
|
|
871
|
+
// get trap via `value.apply(target, args)`), so `this.query` is the correct
|
|
872
|
+
// underlying QueryBuilder with all accumulated WHERE clauses.
|
|
865
873
|
this._cachedUpdateFn = async function timestampedUpdate(data) {
|
|
866
874
|
if (data[modelClass.updatedAt] === undefined) {
|
|
867
875
|
data[modelClass.updatedAt] = new Date();
|
|
868
876
|
}
|
|
869
|
-
return
|
|
877
|
+
return this.query.update(data);
|
|
870
878
|
};
|
|
871
879
|
}
|
|
872
880
|
query.update = this._cachedUpdateFn;
|
|
@@ -169,7 +169,13 @@ export function DataTable(props) {
|
|
|
169
169
|
const [statusFilter, setStatusFilter] = React.useState(urlState.statusFilter);
|
|
170
170
|
const [limit, setLimit] = React.useState(urlState.limit);
|
|
171
171
|
const [columnSearch, setColumnSearch] = React.useState(urlState.columnSearch);
|
|
172
|
-
|
|
172
|
+
// Debounced version of columnSearch — API is only called once the user pauses typing
|
|
173
|
+
const [debouncedColumnSearch, setDebouncedColumnSearch] = React.useState(urlState.columnSearch);
|
|
174
|
+
const columnSearchDebounceRef = React.useRef(null);
|
|
175
|
+
|
|
176
|
+
// Abort controller for in-flight requests — cancelled whenever new fetch params arrive
|
|
177
|
+
const abortControllerRef = React.useRef(null);
|
|
178
|
+
|
|
173
179
|
const [data, setData] = React.useState([]);
|
|
174
180
|
const [loading, setLoading] = React.useState(false);
|
|
175
181
|
const [totalPages, setTotalPages] = React.useState(1);
|
|
@@ -283,6 +289,22 @@ export function DataTable(props) {
|
|
|
283
289
|
}
|
|
284
290
|
}, [location?.search, hasReactRouter, loadStateFromURL]);
|
|
285
291
|
|
|
292
|
+
// Debounce columnSearch — user sees immediate input response, but API call waits 400ms
|
|
293
|
+
// Also syncs back if columnSearch is set programmatically (URL restore / browser back)
|
|
294
|
+
React.useEffect(() => {
|
|
295
|
+
if (columnSearchDebounceRef.current) {
|
|
296
|
+
clearTimeout(columnSearchDebounceRef.current);
|
|
297
|
+
}
|
|
298
|
+
columnSearchDebounceRef.current = setTimeout(() => {
|
|
299
|
+
setDebouncedColumnSearch(columnSearch);
|
|
300
|
+
}, 400);
|
|
301
|
+
return () => {
|
|
302
|
+
if (columnSearchDebounceRef.current) {
|
|
303
|
+
clearTimeout(columnSearchDebounceRef.current);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}, [columnSearch]);
|
|
307
|
+
|
|
286
308
|
const handleStatusToggle = async (row) => {
|
|
287
309
|
if (!toggleLink) return;
|
|
288
310
|
try {
|
|
@@ -298,12 +320,19 @@ export function DataTable(props) {
|
|
|
298
320
|
}
|
|
299
321
|
};
|
|
300
322
|
|
|
301
|
-
// Reset page to 1 when
|
|
323
|
+
// Reset page to 1 when debouncedColumnSearch changes
|
|
302
324
|
React.useEffect(() => {
|
|
303
325
|
setPage(1);
|
|
304
|
-
}, [
|
|
326
|
+
}, [debouncedColumnSearch]);
|
|
305
327
|
|
|
306
328
|
const fetchData = React.useCallback(async () => {
|
|
329
|
+
// Cancel any in-flight request before starting a new one
|
|
330
|
+
if (abortControllerRef.current) {
|
|
331
|
+
abortControllerRef.current.abort();
|
|
332
|
+
}
|
|
333
|
+
abortControllerRef.current = new AbortController();
|
|
334
|
+
const signal = abortControllerRef.current.signal;
|
|
335
|
+
|
|
307
336
|
setLoading(true);
|
|
308
337
|
try {
|
|
309
338
|
const params = new URLSearchParams({
|
|
@@ -314,14 +343,14 @@ export function DataTable(props) {
|
|
|
314
343
|
});
|
|
315
344
|
if (statusFilter !== "all") params.append("isActive", statusFilter);
|
|
316
345
|
if (search) params.append("search", search);
|
|
317
|
-
// Add column search params
|
|
318
|
-
Object.entries(
|
|
346
|
+
// Add debounced column search params
|
|
347
|
+
Object.entries(debouncedColumnSearch).forEach(([field, value]) => {
|
|
319
348
|
if (value) params.append(`columnSearch[${field}]`, value);
|
|
320
349
|
});
|
|
321
350
|
|
|
322
351
|
// Properly append params to apiUrl (check if apiUrl already has query params)
|
|
323
352
|
const separator = apiUrl.includes('?') ? '&' : '?';
|
|
324
|
-
const result = await api.get(`${apiUrl}${separator}${params}
|
|
353
|
+
const result = await api.get(`${apiUrl}${separator}${params}`, { signal });
|
|
325
354
|
|
|
326
355
|
// Handle nested data structure: result.data.data OR result.data.items
|
|
327
356
|
const items = Array.isArray(result.data)
|
|
@@ -333,13 +362,16 @@ export function DataTable(props) {
|
|
|
333
362
|
setTotalPages(pagination?.totalPages || 1);
|
|
334
363
|
setTotalItems(pagination?.total || 0);
|
|
335
364
|
} catch (err) {
|
|
365
|
+
// Ignore abort errors — they are expected when a newer request supersedes this one
|
|
366
|
+
if (err && err.name === 'AbortError') return;
|
|
367
|
+
if (err && err.code === 'ERR_CANCELED') return;
|
|
336
368
|
setData([]);
|
|
337
369
|
setTotalPages(1);
|
|
338
370
|
setTotalItems(0);
|
|
339
371
|
} finally {
|
|
340
372
|
setLoading(false);
|
|
341
373
|
}
|
|
342
|
-
}, [api, apiUrl, page, sortBy, sortOrder, statusFilter, limit, search,
|
|
374
|
+
}, [api, apiUrl, page, sortBy, sortOrder, statusFilter, limit, search, debouncedColumnSearch]);
|
|
343
375
|
|
|
344
376
|
// Trigger fetchData for main params
|
|
345
377
|
React.useEffect(() => {
|
|
@@ -360,6 +392,15 @@ export function DataTable(props) {
|
|
|
360
392
|
}
|
|
361
393
|
}, [refreshKey, fetchData]);
|
|
362
394
|
|
|
395
|
+
// Abort any in-flight request when the component unmounts
|
|
396
|
+
React.useEffect(() => {
|
|
397
|
+
return () => {
|
|
398
|
+
if (abortControllerRef.current) {
|
|
399
|
+
abortControllerRef.current.abort();
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}, []);
|
|
403
|
+
|
|
363
404
|
const handleSort = (field) => {
|
|
364
405
|
if (sortBy === field) {
|
|
365
406
|
setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
|
package/jsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vasuzex",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.13",
|
|
4
4
|
"description": "Laravel-inspired framework for Node.js monorepos - V2 with optimized dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./framework/index.js",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"express-rate-limit": "^8.2.1",
|
|
109
109
|
"firebase-admin": "^13.6.0",
|
|
110
110
|
"fs-extra": "^11.3.2",
|
|
111
|
-
"guruorm": "^2.1.
|
|
111
|
+
"guruorm": "^2.1.24",
|
|
112
112
|
"helmet": "^8.1.0",
|
|
113
113
|
"inquirer": "^9.3.8",
|
|
114
114
|
"ip2location-nodejs": "^9.7.0",
|