tempest-react-sdk 0.48.0 → 0.49.0
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/dist/audio/sfx-pool.cjs +1 -1
- package/dist/audio/sfx-pool.cjs.map +1 -1
- package/dist/audio/sfx-pool.js +16 -7
- package/dist/audio/sfx-pool.js.map +1 -1
- package/dist/audio/use-sfx-pool.cjs +1 -1
- package/dist/audio/use-sfx-pool.cjs.map +1 -1
- package/dist/audio/use-sfx-pool.js +3 -8
- package/dist/audio/use-sfx-pool.js.map +1 -1
- package/dist/auth/passkey.cjs +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js +79 -81
- package/dist/auth/passkey.js.map +1 -1
- package/dist/charts.cjs +1 -1
- package/dist/charts.js +4 -4
- package/dist/components/BarList/BarList.cjs +1 -1
- package/dist/components/BarList/BarList.cjs.map +1 -1
- package/dist/components/BarList/BarList.js +31 -24
- package/dist/components/BarList/BarList.js.map +1 -1
- package/dist/components/BarList/bar-list-model.cjs +1 -1
- package/dist/components/BarList/bar-list-model.cjs.map +1 -1
- package/dist/components/BarList/bar-list-model.js +5 -5
- package/dist/components/BarList/bar-list-model.js.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.js +13 -14
- package/dist/components/ClickOutside/ClickOutside.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +124 -122
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/FilterBar/filter-apply.cjs +1 -1
- package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
- package/dist/components/FilterBar/filter-apply.js +35 -38
- package/dist/components/FilterBar/filter-apply.js.map +1 -1
- package/dist/components/FilterBar/filter-query.cjs +1 -1
- package/dist/components/FilterBar/filter-query.cjs.map +1 -1
- package/dist/components/FilterBar/filter-query.js +16 -13
- package/dist/components/FilterBar/filter-query.js.map +1 -1
- package/dist/geo/use-position-tracker.cjs +1 -1
- package/dist/geo/use-position-tracker.cjs.map +1 -1
- package/dist/geo/use-position-tracker.js +25 -26
- package/dist/geo/use-position-tracker.js.map +1 -1
- package/dist/hooks/use-async.cjs +1 -1
- package/dist/hooks/use-async.cjs.map +1 -1
- package/dist/hooks/use-async.js +27 -30
- package/dist/hooks/use-async.js.map +1 -1
- package/dist/hooks/use-click-outside.cjs +1 -1
- package/dist/hooks/use-click-outside.cjs.map +1 -1
- package/dist/hooks/use-click-outside.js +8 -9
- package/dist/hooks/use-click-outside.js.map +1 -1
- package/dist/hooks/use-countdown.cjs +1 -1
- package/dist/hooks/use-countdown.cjs.map +1 -1
- package/dist/hooks/use-countdown.js +8 -13
- package/dist/hooks/use-countdown.js.map +1 -1
- package/dist/hooks/use-event-listener.cjs +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-event-listener.js +6 -6
- package/dist/hooks/use-event-listener.js.map +1 -1
- package/dist/hooks/use-interval.cjs +1 -1
- package/dist/hooks/use-interval.cjs.map +1 -1
- package/dist/hooks/use-interval.js +5 -6
- package/dist/hooks/use-interval.js.map +1 -1
- package/dist/hooks/use-latest-ref.cjs.map +1 -1
- package/dist/hooks/use-latest-ref.js.map +1 -1
- package/dist/hooks/use-long-press-handlers.cjs +1 -1
- package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
- package/dist/hooks/use-long-press-handlers.js +8 -10
- package/dist/hooks/use-long-press-handlers.js.map +1 -1
- package/dist/hooks/use-long-press.cjs +1 -1
- package/dist/hooks/use-long-press.cjs.map +1 -1
- package/dist/hooks/use-long-press.js +6 -6
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-online.cjs +1 -1
- package/dist/hooks/use-online.cjs.map +1 -1
- package/dist/hooks/use-online.js +10 -10
- package/dist/hooks/use-online.js.map +1 -1
- package/dist/hooks/use-service-worker-update.cjs +1 -1
- package/dist/hooks/use-service-worker-update.cjs.map +1 -1
- package/dist/hooks/use-service-worker-update.js +24 -25
- package/dist/hooks/use-service-worker-update.js.map +1 -1
- package/dist/hooks/use-stable-callback.cjs +1 -1
- package/dist/hooks/use-stable-callback.cjs.map +1 -1
- package/dist/hooks/use-stable-callback.js +4 -3
- package/dist/hooks/use-stable-callback.js.map +1 -1
- package/dist/hooks/use-timeout.cjs +1 -1
- package/dist/hooks/use-timeout.cjs.map +1 -1
- package/dist/hooks/use-timeout.js +5 -6
- package/dist/hooks/use-timeout.js.map +1 -1
- package/dist/hooks/use-typewriter.cjs +1 -1
- package/dist/hooks/use-typewriter.cjs.map +1 -1
- package/dist/hooks/use-typewriter.js +9 -18
- package/dist/hooks/use-typewriter.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +11 -16
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/describe-api-error.cjs +1 -1
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js +12 -15
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +18 -6
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +60 -61
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/retry.cjs +1 -1
- package/dist/http/retry.cjs.map +1 -1
- package/dist/http/retry.js +20 -16
- package/dist/http/retry.js.map +1 -1
- package/dist/http/use-describe-api-error.cjs +1 -1
- package/dist/http/use-describe-api-error.cjs.map +1 -1
- package/dist/http/use-describe-api-error.js +2 -2
- package/dist/http/use-describe-api-error.js.map +1 -1
- package/dist/i18n/create-i18n.cjs +1 -1
- package/dist/i18n/create-i18n.cjs.map +1 -1
- package/dist/i18n/create-i18n.js +6 -6
- package/dist/i18n/create-i18n.js.map +1 -1
- package/dist/icons/material-symbols.cjs +1 -1
- package/dist/icons/material-symbols.cjs.map +1 -1
- package/dist/icons/material-symbols.js +136 -5
- package/dist/icons/material-symbols.js.map +1 -1
- package/dist/icons.d.ts +17 -5
- package/dist/offline/create-offline-sync.cjs +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js +17 -9
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/offline/use-offline-sync.cjs +1 -1
- package/dist/offline/use-offline-sync.cjs.map +1 -1
- package/dist/offline/use-offline-sync.js +7 -8
- package/dist/offline/use-offline-sync.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +13 -12
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/query/retry-policy.cjs +1 -1
- package/dist/query/retry-policy.cjs.map +1 -1
- package/dist/query/retry-policy.js +3 -3
- package/dist/query/retry-policy.js.map +1 -1
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +19 -20
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/tabular/use-tabular-predictor.cjs +1 -1
- package/dist/tabular/use-tabular-predictor.cjs.map +1 -1
- package/dist/tabular/use-tabular-predictor.js +20 -17
- package/dist/tabular/use-tabular-predictor.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +363 -149
- package/dist/tempest-react-sdk.js +143 -143
- package/dist/theme/ThemeProvider.cjs +1 -1
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.js +12 -17
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/utils/base64.cjs +2 -0
- package/dist/utils/base64.cjs.map +1 -0
- package/dist/utils/base64.js +16 -0
- package/dist/utils/base64.js.map +1 -0
- package/dist/utils/compare-values.cjs +1 -1
- package/dist/utils/compare-values.cjs.map +1 -1
- package/dist/utils/compare-values.js +4 -3
- package/dist/utils/compare-values.js.map +1 -1
- package/dist/utils/compressed-storage.cjs +1 -1
- package/dist/utils/compressed-storage.cjs.map +1 -1
- package/dist/utils/compressed-storage.js +16 -46
- package/dist/utils/compressed-storage.js.map +1 -1
- package/dist/utils/storage.cjs +1 -1
- package/dist/utils/storage.cjs.map +1 -1
- package/dist/utils/storage.js +37 -17
- package/dist/utils/storage.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-camera-stream.js +34 -33
- package/dist/vision/use-camera-stream.js.map +1 -1
- package/dist/vite/base-url.cjs +2 -0
- package/dist/vite/base-url.cjs.map +1 -0
- package/dist/vite/base-url.js +8 -0
- package/dist/vite/base-url.js.map +1 -0
- package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.js +54 -29
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs +2 -2
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +15 -14
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/vite.d.ts +2 -2
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +40 -42
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,79 +11,81 @@ import { LoadingRows as ee } from "./LoadingRows.js";
|
|
|
11
11
|
import { useDevWarnings as te } from "./use-dev-warnings.js";
|
|
12
12
|
import { DEFAULT_EDIT_LABELS as ne } from "./edit-labels.js";
|
|
13
13
|
import { useCallback as l, useEffect as re, useMemo as u, useState as d } from "react";
|
|
14
|
-
import { jsx as f, jsxs as
|
|
14
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
15
15
|
//#region src/components/DataTable/DataTable.tsx
|
|
16
|
-
function
|
|
16
|
+
function ie(e, t) {
|
|
17
17
|
return `${String(e)}::${String(t)}`;
|
|
18
18
|
}
|
|
19
|
-
function
|
|
19
|
+
function m(e) {
|
|
20
20
|
return typeof e.header == "string" ? e.header : String(e.key);
|
|
21
21
|
}
|
|
22
|
-
function
|
|
23
|
-
let [T, he] = d(""), [E, ge] = d(
|
|
24
|
-
|
|
22
|
+
function h({ data: h, columns: g, pageSize: _ = 10, searchable: v = !1, searchKeys: y, initialSort: ae, rowKey: b = (e, t) => t, emptyMessage: oe, onCellChange: x, editLabels: se, totalItems: ce, page: S, onPageChange: C, manualSort: le, onSortChange: ue, manualSearch: de, onSearchChange: fe, loading: w = !1, className: pe, ...me }) {
|
|
23
|
+
let [T, he] = d(""), [E, ge] = d(ae ?? null), { page: _e, setPage: D } = t(1, _), O = n(), k = ce !== void 0, A = le ?? k, j = de ?? k, M = S ?? _e, N = l((e) => {
|
|
24
|
+
S === void 0 && D(e), C?.(e);
|
|
25
25
|
}, [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
S,
|
|
27
|
+
D,
|
|
28
|
+
C
|
|
29
29
|
]);
|
|
30
30
|
te({
|
|
31
|
-
serverMode:
|
|
32
|
-
controlledPage:
|
|
33
|
-
onPageChange:
|
|
34
|
-
sortIsManual:
|
|
35
|
-
onSortChange:
|
|
31
|
+
serverMode: k,
|
|
32
|
+
controlledPage: S,
|
|
33
|
+
onPageChange: C,
|
|
34
|
+
sortIsManual: A,
|
|
35
|
+
onSortChange: ue
|
|
36
36
|
});
|
|
37
|
-
let [
|
|
37
|
+
let [P, F] = d(null), [I, L] = d(null), [R, z] = d({}), [B, V] = d({}), [H, ve] = d({}), U = u(() => ({
|
|
38
38
|
...ne,
|
|
39
|
-
...
|
|
40
|
-
}), [
|
|
41
|
-
let t =
|
|
39
|
+
...se
|
|
40
|
+
}), [se]), W = x !== void 0 && g.some((e) => e.editable), ye = u(() => !v || j ? [] : y && y.length > 0 ? y : g.filter((e) => {
|
|
41
|
+
let t = h.find((t) => t[e.key] != null), n = t ? t[e.key] : void 0;
|
|
42
42
|
return typeof n == "string" || typeof n == "number";
|
|
43
43
|
}).map((e) => e.key), [
|
|
44
44
|
v,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
j,
|
|
46
|
+
y,
|
|
47
|
+
g,
|
|
48
|
+
h
|
|
49
|
+
]), G = u(() => {
|
|
48
50
|
let e = T.trim().toLowerCase();
|
|
49
|
-
return !e || !
|
|
51
|
+
return !e || !v || j ? h : h.filter((t) => ye.some((n) => {
|
|
50
52
|
let r = t[n];
|
|
51
53
|
return r != null && String(r).toLowerCase().includes(e);
|
|
52
54
|
}));
|
|
53
55
|
}, [
|
|
54
|
-
|
|
56
|
+
h,
|
|
55
57
|
T,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
]),
|
|
60
|
-
if (!E ||
|
|
58
|
+
v,
|
|
59
|
+
j,
|
|
60
|
+
ye
|
|
61
|
+
]), K = u(() => {
|
|
62
|
+
if (!E || A) return G;
|
|
61
63
|
let e = E.direction === "asc" ? 1 : -1;
|
|
62
|
-
return [...
|
|
64
|
+
return [...G].sort((t, n) => o(t[E.key], n[E.key]) * e);
|
|
63
65
|
}, [
|
|
64
|
-
|
|
66
|
+
G,
|
|
65
67
|
E,
|
|
66
|
-
|
|
67
|
-
]),
|
|
68
|
+
A
|
|
69
|
+
]), be = ce ?? K.length, q = Math.max(1, Math.ceil(be / _));
|
|
68
70
|
re(() => {
|
|
69
|
-
!
|
|
71
|
+
!k && M > q && N(q);
|
|
70
72
|
}, [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
k,
|
|
74
|
+
M,
|
|
75
|
+
q,
|
|
76
|
+
N
|
|
75
77
|
]);
|
|
76
|
-
let
|
|
77
|
-
if (
|
|
78
|
-
let e = (
|
|
79
|
-
return
|
|
78
|
+
let J = k ? M : Math.min(M, q), Y = u(() => {
|
|
79
|
+
if (k) return K;
|
|
80
|
+
let e = (J - 1) * _;
|
|
81
|
+
return K.slice(e, e + _);
|
|
80
82
|
}, [
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
k,
|
|
84
|
+
K,
|
|
85
|
+
J,
|
|
86
|
+
_
|
|
85
87
|
]);
|
|
86
|
-
function
|
|
88
|
+
function xe(e) {
|
|
87
89
|
let t = E, n = !t || t.key !== e ? {
|
|
88
90
|
key: e,
|
|
89
91
|
direction: "asc"
|
|
@@ -91,96 +93,96 @@ function m({ data: m, columns: h, pageSize: g = 10, searchable: _ = !1, searchKe
|
|
|
91
93
|
key: e,
|
|
92
94
|
direction: "desc"
|
|
93
95
|
} : null;
|
|
94
|
-
ge(n),
|
|
96
|
+
ge(n), ue?.(n);
|
|
95
97
|
}
|
|
96
|
-
let
|
|
97
|
-
if (!
|
|
98
|
+
let X = l((e) => (J - 1) * _ + e, [J, _]), Z = u(() => {
|
|
99
|
+
if (!W) return [];
|
|
98
100
|
let e = [];
|
|
99
|
-
return
|
|
100
|
-
let r =
|
|
101
|
-
for (let t of
|
|
101
|
+
return Y.forEach((t, n) => {
|
|
102
|
+
let r = b(t, X(n));
|
|
103
|
+
for (let t of g) t.editable && e.push(ie(r, t.key));
|
|
102
104
|
}), e;
|
|
103
105
|
}, [
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
]),
|
|
109
|
-
|
|
106
|
+
W,
|
|
107
|
+
Y,
|
|
108
|
+
g,
|
|
109
|
+
X
|
|
110
|
+
]), Q = l((e, t, n) => n in R ? R[n] : e[t.key], [R]), Se = l(async (e, t, n) => {
|
|
111
|
+
ve((t) => ({
|
|
110
112
|
...t,
|
|
111
113
|
[e]: !0
|
|
112
114
|
}));
|
|
113
115
|
try {
|
|
114
|
-
await
|
|
116
|
+
await x?.(n), O(U.saved(m(t)));
|
|
115
117
|
} catch (n) {
|
|
116
|
-
|
|
118
|
+
z((t) => {
|
|
117
119
|
let n = { ...t };
|
|
118
120
|
return delete n[e], n;
|
|
119
121
|
});
|
|
120
|
-
let r = n instanceof Error && n.message ? n.message :
|
|
121
|
-
|
|
122
|
+
let r = n instanceof Error && n.message ? n.message : U.saveFailed(m(t));
|
|
123
|
+
V((t) => ({
|
|
122
124
|
...t,
|
|
123
125
|
[e]: r
|
|
124
126
|
}));
|
|
125
127
|
} finally {
|
|
126
|
-
|
|
128
|
+
ve((t) => {
|
|
127
129
|
let n = { ...t };
|
|
128
130
|
return delete n[e], n;
|
|
129
131
|
});
|
|
130
132
|
}
|
|
131
133
|
}, [
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
]),
|
|
134
|
+
x,
|
|
135
|
+
O,
|
|
136
|
+
U
|
|
137
|
+
]), $ = l((e, t) => {
|
|
136
138
|
if (t === "none") {
|
|
137
|
-
|
|
139
|
+
F(null), L(e);
|
|
138
140
|
return;
|
|
139
141
|
}
|
|
140
|
-
let n =
|
|
142
|
+
let n = Z.indexOf(e), r = Z[n + (t === "next" ? 1 : -1)];
|
|
141
143
|
if (r === void 0) {
|
|
142
|
-
|
|
144
|
+
F(null), L(e);
|
|
143
145
|
return;
|
|
144
146
|
}
|
|
145
|
-
|
|
146
|
-
}, [
|
|
147
|
-
let o =
|
|
147
|
+
F(r), L(null);
|
|
148
|
+
}, [Z]), Ce = l((e, t, n, r, i, a) => {
|
|
149
|
+
let o = Q(e, t, n);
|
|
148
150
|
if (i === (t.formatEdit ? t.formatEdit(e) : String(o ?? ""))) {
|
|
149
|
-
|
|
151
|
+
$(n, a);
|
|
150
152
|
return;
|
|
151
153
|
}
|
|
152
154
|
let s = t.parse ? t.parse(i, e) : t.editorType === "number" ? Number(i) : i.trim(), c = t.validate?.(s, e) ?? null;
|
|
153
155
|
if (c) {
|
|
154
|
-
|
|
156
|
+
V((e) => ({
|
|
155
157
|
...e,
|
|
156
158
|
[n]: c
|
|
157
159
|
}));
|
|
158
160
|
return;
|
|
159
161
|
}
|
|
160
|
-
|
|
162
|
+
V((e) => {
|
|
161
163
|
let t = { ...e };
|
|
162
164
|
return delete t[n], t;
|
|
163
|
-
}),
|
|
165
|
+
}), z((e) => ({
|
|
164
166
|
...e,
|
|
165
167
|
[n]: s
|
|
166
|
-
})),
|
|
168
|
+
})), $(n, a), Se(n, t, {
|
|
167
169
|
row: e,
|
|
168
170
|
key: t.key,
|
|
169
171
|
value: s,
|
|
170
172
|
previous: o,
|
|
171
|
-
rowIndex:
|
|
173
|
+
rowIndex: X(r)
|
|
172
174
|
});
|
|
173
175
|
}, [
|
|
174
|
-
Z,
|
|
175
176
|
Q,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
$,
|
|
178
|
+
Se,
|
|
179
|
+
X
|
|
180
|
+
]), we = u(() => g.map((e) => {
|
|
181
|
+
let t = E?.key === e.key ? E?.direction === "asc" ? " ▲" : " ▼" : "", n = e.sortable ? /* @__PURE__ */ p("button", {
|
|
180
182
|
type: "button",
|
|
181
183
|
className: s.sortButton,
|
|
182
|
-
onClick: () =>
|
|
183
|
-
"aria-label": `Ordenar por ${
|
|
184
|
+
onClick: () => xe(e.key),
|
|
185
|
+
"aria-label": `Ordenar por ${m(e)}`,
|
|
184
186
|
children: [e.header, /* @__PURE__ */ f("span", {
|
|
185
187
|
className: s.sortIndicator,
|
|
186
188
|
"aria-hidden": !0,
|
|
@@ -192,7 +194,7 @@ function m({ data: m, columns: h, pageSize: g = 10, searchable: _ = !1, searchKe
|
|
|
192
194
|
[e.key]: n
|
|
193
195
|
};
|
|
194
196
|
return e.render ? e.render(r) : r[e.key] ?? null;
|
|
195
|
-
}, a =
|
|
197
|
+
}, a = W && e.editable === !0;
|
|
196
198
|
return {
|
|
197
199
|
key: String(e.key),
|
|
198
200
|
header: n,
|
|
@@ -200,24 +202,24 @@ function m({ data: m, columns: h, pageSize: g = 10, searchable: _ = !1, searchKe
|
|
|
200
202
|
priority: e.priority,
|
|
201
203
|
width: e.width,
|
|
202
204
|
render: a ? (t, n) => {
|
|
203
|
-
let r =
|
|
205
|
+
let r = ie(b(t, X(n)), e.key), a = Q(t, e, r), o = e.formatEdit ? e.formatEdit(t) : String(a ?? "");
|
|
204
206
|
return /* @__PURE__ */ f(c, {
|
|
205
207
|
text: o,
|
|
206
|
-
columnLabel:
|
|
208
|
+
columnLabel: m(e),
|
|
207
209
|
rowNumber: n + 1,
|
|
208
210
|
inputType: e.editorType ?? "text",
|
|
209
|
-
editing:
|
|
210
|
-
refocus:
|
|
211
|
-
saving:
|
|
212
|
-
error:
|
|
211
|
+
editing: P === r,
|
|
212
|
+
refocus: I === r,
|
|
213
|
+
saving: H[r] === !0,
|
|
214
|
+
error: B[r] ?? null,
|
|
213
215
|
errorId: `tempest-cell-error-${r.replace(/[^\w-]/g, "_")}`,
|
|
214
|
-
labels:
|
|
216
|
+
labels: U,
|
|
215
217
|
onOpen: () => {
|
|
216
|
-
|
|
218
|
+
F(r), L(null);
|
|
217
219
|
},
|
|
218
|
-
onCommit: (i, a) =>
|
|
220
|
+
onCommit: (i, a) => Ce(t, e, r, n, i, a),
|
|
219
221
|
onCancel: () => {
|
|
220
|
-
|
|
222
|
+
F(null), L(r), V((e) => {
|
|
221
223
|
let t = { ...e };
|
|
222
224
|
return delete t[r], t;
|
|
223
225
|
});
|
|
@@ -227,53 +229,53 @@ function m({ data: m, columns: h, pageSize: g = 10, searchable: _ = !1, searchKe
|
|
|
227
229
|
} : r
|
|
228
230
|
};
|
|
229
231
|
}), [
|
|
230
|
-
|
|
232
|
+
g,
|
|
231
233
|
E,
|
|
232
|
-
|
|
233
|
-
M,
|
|
234
|
+
W,
|
|
234
235
|
P,
|
|
236
|
+
I,
|
|
237
|
+
H,
|
|
235
238
|
B,
|
|
239
|
+
U,
|
|
236
240
|
R,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
]), $ = w && J.length === 0;
|
|
242
|
-
return /* @__PURE__ */ ie("div", {
|
|
241
|
+
X,
|
|
242
|
+
Ce
|
|
243
|
+
]), Te = w && Y.length === 0;
|
|
244
|
+
return /* @__PURE__ */ p("div", {
|
|
243
245
|
className: e(s.wrapper, pe),
|
|
244
246
|
...me,
|
|
245
247
|
children: [
|
|
246
|
-
|
|
248
|
+
v && /* @__PURE__ */ f(i, {
|
|
247
249
|
value: T,
|
|
248
250
|
onChange: (e) => {
|
|
249
|
-
he(e),
|
|
251
|
+
he(e), N(1), fe?.(e);
|
|
250
252
|
},
|
|
251
253
|
wrapperClassName: s.search
|
|
252
254
|
}),
|
|
253
255
|
/* @__PURE__ */ f("div", {
|
|
254
|
-
className: e(w &&
|
|
256
|
+
className: e(w && !Te && s.pending),
|
|
255
257
|
"aria-busy": w || void 0,
|
|
256
258
|
"data-testid": "tempest-datatable-body",
|
|
257
|
-
children:
|
|
258
|
-
columns:
|
|
259
|
-
rows: Math.min(
|
|
259
|
+
children: Te ? /* @__PURE__ */ f(ee, {
|
|
260
|
+
columns: we.length,
|
|
261
|
+
rows: Math.min(_, 8)
|
|
260
262
|
}) : /* @__PURE__ */ f(a, {
|
|
261
|
-
columns:
|
|
262
|
-
data:
|
|
263
|
-
rowKey: (e, t) =>
|
|
264
|
-
emptyMessage:
|
|
263
|
+
columns: we,
|
|
264
|
+
data: Y,
|
|
265
|
+
rowKey: (e, t) => b(e, X(t)),
|
|
266
|
+
emptyMessage: oe
|
|
265
267
|
})
|
|
266
268
|
}),
|
|
267
|
-
|
|
268
|
-
page:
|
|
269
|
-
totalPages:
|
|
270
|
-
onPageChange:
|
|
271
|
-
totalItems:
|
|
269
|
+
q > 1 && /* @__PURE__ */ f(r, {
|
|
270
|
+
page: J,
|
|
271
|
+
totalPages: q,
|
|
272
|
+
onPageChange: N,
|
|
273
|
+
totalItems: be
|
|
272
274
|
})
|
|
273
275
|
]
|
|
274
276
|
});
|
|
275
277
|
}
|
|
276
278
|
//#endregion
|
|
277
|
-
export {
|
|
279
|
+
export { h as DataTable };
|
|
278
280
|
|
|
279
281
|
//# sourceMappingURL=DataTable.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the table does four jobs\n * a caller turns on independently — paging (pageSize), search (searchable,\n * searchKeys), sort (initialSort) and inline edit (onCellChange, editLabels) — over\n * one row model (data, columns, rowKey, emptyMessage). The body is long because\n * those four share the derived-rows pipeline: filter, then sort, then page, then map\n * to cells, in that order and off the same memo.\n */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\nimport { usePagination } from \"@/hooks\";\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { Table, type TableAlign, type TableColumn, type TablePriority } from \"../Table\";\nimport { Pagination } from \"../Pagination\";\nimport { SearchBar } from \"../SearchBar\";\nimport { EditableCell } from \"./EditableCell\";\nimport { LoadingRows } from \"./LoadingRows\";\nimport { useDevWarnings } from \"./use-dev-warnings\";\nimport { DEFAULT_EDIT_LABELS, type CellCommitMove, type DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort<T> {\n key: keyof T;\n direction: SortDirection;\n}\n\n/** Input types an editable column can use. */\nexport type DataTableEditorType = \"text\" | \"number\" | \"date\" | \"email\" | \"tel\" | \"url\";\n\n/** One accepted cell edit, handed to `onCellChange`. */\nexport interface DataTableCellChange<T> {\n /** The row as it was before the edit. */\n row: T;\n /** Which column changed. */\n key: keyof T;\n /** The parsed new value. */\n value: unknown;\n /** The value that was displayed before the edit. */\n previous: unknown;\n /** Index of the row in the full `data` array. */\n rowIndex: number;\n}\n\n/**\n * Column definition for {@link DataTable}. Extends the headless {@link Table}\n * column shape with a typed `key`, opt-in sorting, opt-in inline editing, and the\n * visual options that are forwarded to the underlying Table cell.\n */\nexport interface DataTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `String(row[key])`. */\n render?: (row: T) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment forwarded to the Table cell. */\n align?: TableAlign;\n /** Responsive visibility priority forwarded to the Table cell. */\n priority?: TablePriority;\n /** Fixed column width forwarded to the Table cell. */\n width?: string | number;\n /**\n * Let cells in this column be edited in place. Requires `onCellChange` on the\n * table; without it the column stays read-only.\n */\n editable?: boolean;\n /** Editor input type. Default `\"text\"`. */\n editorType?: DataTableEditorType;\n /** Text the editor opens with. Defaults to `String(value ?? \"\")`. */\n formatEdit?: (row: T) => string;\n /**\n * Turn the typed string into the stored value. Defaults to the trimmed string,\n * or `Number(raw)` when `editorType` is `\"number\"`.\n */\n parse?: (raw: string, row: T) => unknown;\n /** Return a message to reject the edit, or `null` to accept it. */\n validate?: (value: unknown, row: T) => string | null;\n}\n\nexport interface DataTableProps<T> extends HTMLAttributes<HTMLDivElement> {\n /**\n * The rows to work with.\n *\n * By default this is the **full** dataset and sorting, searching and paging\n * all happen in memory. Pass `totalItems` and it becomes the current page as\n * the server returned it, with those three delegated to the caller.\n */\n data: T[];\n /** Column definitions. */\n columns: DataTableColumn<T>[];\n /** Rows per page. Default 10. */\n pageSize?: number;\n /** Render a search input above the table. Default false. */\n searchable?: boolean;\n /**\n * Keys to match the search term against. When omitted, every column whose\n * value is a string or number is searched.\n */\n searchKeys?: (keyof T)[];\n /** Initial sort applied before any header interaction. */\n initialSort?: DataTableSort<T>;\n /** Stable key extractor for rows. Defaults to the row index. */\n rowKey?: (row: T, index: number) => string | number;\n /** Content shown when no rows match. */\n emptyMessage?: ReactNode;\n /**\n * Persist an accepted cell edit. Return a promise: while it is pending the cell\n * already shows the new value, and a rejection rolls that back and surfaces the\n * error in the cell. Without this prop no column is editable.\n */\n onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;\n /** Override the PT-BR copy of the editing affordances. */\n editLabels?: Partial<DataTableEditLabels>;\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). Pair it with\n * `page` and `onPageChange`.\n */\n totalItems?: number;\n /** Current page, 1-based. Controlled — required in server mode. */\n page?: number;\n /** Called with the next page. Required whenever `page` is controlled. */\n onPageChange?: (page: number) => void;\n /**\n * Sorting is the caller's job: clicking a header reports through\n * `onSortChange` and the rows are left in the order they arrived.\n *\n * Implied by `totalItems`, because sorting the page in memory would sort\n * *that page only* while the header claims the whole table is ordered.\n */\n manualSort?: boolean;\n /** Called with the next sort state — `null` when the header cycles back to unsorted. */\n onSortChange?: (sort: DataTableSort<T> | null) => void;\n /**\n * Searching is the caller's job: typing reports through `onSearchChange` and\n * the rows are left as they arrived.\n *\n * Implied by `totalItems`. Filtering the current page would hide the rows\n * that do not match *on this page* and show nothing for a term that only\n * matches on page three — an empty table that looks like \"no results\".\n */\n manualSearch?: boolean;\n /** Called with the current search term (debouncing, if any, is the caller's). */\n onSearchChange?: (term: string) => void;\n /**\n * A fetch is in flight.\n *\n * With rows already on screen they stay put, dimmed and `aria-busy`, so the\n * page does not jump under the cursor between pages. With no rows yet it\n * renders placeholder lines at full height, which is a different statement\n * from `emptyMessage`: \"loading\" and \"there is nothing\" are not the same\n * screen.\n */\n loading?: boolean;\n}\n\n/** Identity of one cell, stable across re-renders and pagination. */\nfunction cellId(rowKeyValue: string | number, columnKey: PropertyKey): string {\n return `${String(rowKeyValue)}::${String(columnKey)}`;\n}\n\nfunction headerText<T>(column: DataTableColumn<T>): string {\n return typeof column.header === \"string\" ? column.header : String(column.key);\n}\n\n/**\n * Stateful, headless data table built on top of {@link Table}. Adds\n * client-side searching, click-to-sort columns, pagination and opt-in inline\n * editing while delegating all table markup to the underlying Table component.\n *\n * - Clicking a sortable header cycles asc → desc → unsorted.\n * - Search matches a case-insensitive substring across `searchKeys`\n * (or every string/number column when not provided).\n * - Pagination is hidden when the result fits on a single page.\n * - A column with `editable` renders a button that opens an inline editor;\n * `Enter` commits, `Escape` discards, `Tab` walks to the next editable cell.\n *\n * Editing is strictly opt-in: with no `editable` column (or no `onCellChange`) the\n * rendered markup is byte-for-byte what it was before the feature existed, which\n * matters because the component is published.\n *\n * ## Optimistic, with a visible rollback\n *\n * An accepted edit is shown immediately and `onCellChange` runs in the background.\n * If it rejects, the cell returns to the old value **and** shows the reason as a\n * `role=\"alert\"` tied to the cell. A silent revert is worse than no optimistic\n * update at all: the user watched their edit appear and has no reason to doubt it.\n *\n * The header memo depends on `columns`, `sort` and the editing state only:\n * `toggleSort` and the commit callbacks are recreated each render but always close\n * over the same setters, so including them would rebuild every header on every\n * render without changing behaviour. That is why `exhaustive-deps` is silenced on\n * that dependency array.\n */\nexport function DataTable<T>({\n data,\n columns,\n pageSize = 10,\n searchable = false,\n searchKeys,\n initialSort,\n rowKey = (_row, index) => index,\n emptyMessage,\n onCellChange,\n editLabels,\n totalItems,\n page: controlledPage,\n onPageChange,\n manualSort,\n onSortChange,\n manualSearch,\n onSearchChange,\n loading = false,\n className,\n ...rest\n}: DataTableProps<T>) {\n const [search, setSearch] = useState<string>(\"\");\n const [sort, setSort] = useState<DataTableSort<T> | null>(initialSort ?? null);\n const { page: internalPage, setPage: setInternalPage } = usePagination(1, pageSize);\n const announce = useAnnounce();\n\n const serverMode = totalItems !== undefined;\n const sortIsManual = manualSort ?? serverMode;\n const searchIsManual = manualSearch ?? serverMode;\n const page = controlledPage ?? internalPage;\n\n const setPage = useCallback(\n (next: number) => {\n if (controlledPage === undefined) setInternalPage(next);\n onPageChange?.(next);\n },\n [controlledPage, setInternalPage, onPageChange],\n );\n\n useDevWarnings({ serverMode, controlledPage, onPageChange, sortIsManual, onSortChange });\n\n const [editing, setEditing] = useState<string | null>(null);\n const [refocus, setRefocus] = useState<string | null>(null);\n const [overrides, setOverrides] = useState<Record<string, unknown>>({});\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [saving, setSaving] = useState<Record<string, boolean>>({});\n\n const labels = useMemo<DataTableEditLabels>(\n () => ({ ...DEFAULT_EDIT_LABELS, ...editLabels }),\n [editLabels],\n );\n const editingEnabled = onCellChange !== undefined && columns.some((column) => column.editable);\n\n const effectiveSearchKeys = useMemo<(keyof T)[]>(() => {\n if (searchKeys && searchKeys.length > 0) return searchKeys;\n return columns\n .filter((column) => {\n const sample = data.find((row) => row[column.key] != null);\n const value = sample ? sample[column.key] : undefined;\n return typeof value === \"string\" || typeof value === \"number\";\n })\n .map((column) => column.key);\n }, [searchKeys, columns, data]);\n\n const filtered = useMemo<T[]>(() => {\n const term = search.trim().toLowerCase();\n if (!term || !searchable || searchIsManual) return data;\n return data.filter((row) =>\n effectiveSearchKeys.some((key) => {\n const value = row[key];\n return value != null && String(value).toLowerCase().includes(term);\n }),\n );\n }, [data, search, searchable, searchIsManual, effectiveSearchKeys]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort || sortIsManual) return filtered;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...filtered].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [filtered, sort, sortIsManual]);\n\n const rowCount = totalItems ?? sorted.length;\n const totalPages = Math.max(1, Math.ceil(rowCount / pageSize));\n\n /**\n * Clamp the current page when the dataset shrinks (e.g. after filtering).\n *\n * Skipped in server mode: `page` belongs to the caller there, and a clamp\n * fired against a `totalItems` that has not caught up with the new filter\n * yet would send them a page they did not ask for, mid-fetch.\n */\n useEffect(() => {\n if (!serverMode && page > totalPages) setPage(totalPages);\n }, [serverMode, page, totalPages, setPage]);\n\n const safePage = serverMode ? page : Math.min(page, totalPages);\n const pageRows = useMemo<T[]>(() => {\n if (serverMode) return sorted;\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [serverMode, sorted, safePage, pageSize]);\n\n function toggleSort(key: keyof T): void {\n const current = sort;\n const next: DataTableSort<T> | null =\n !current || current.key !== key\n ? { key, direction: \"asc\" }\n : current.direction === \"asc\"\n ? { key, direction: \"desc\" }\n : null;\n setSort(next);\n onSortChange?.(next);\n }\n\n const absoluteIndex = useCallback(\n (pageIndex: number) => (safePage - 1) * pageSize + pageIndex,\n [safePage, pageSize],\n );\n\n /**\n * Every editable cell on the page, row-major — the order `Tab` walks.\n *\n * Row-major and not column-major because a row is the record a user is\n * correcting; walking down a column would make them re-find their place on\n * every keystroke.\n */\n const editableCellIds = useMemo<string[]>(() => {\n if (!editingEnabled) return [];\n const ids: string[] = [];\n pageRows.forEach((row, index) => {\n const key = rowKey(row, absoluteIndex(index));\n for (const column of columns) {\n if (column.editable) ids.push(cellId(key, column.key));\n }\n });\n return ids;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editingEnabled, pageRows, columns, absoluteIndex]);\n\n const displayed = useCallback(\n (row: T, column: DataTableColumn<T>, id: string): unknown =>\n id in overrides ? overrides[id] : row[column.key],\n [overrides],\n );\n\n /**\n * Run the caller's `onCellChange` behind the optimistic update.\n *\n * Only the *success* is announced. The failure already renders as a\n * `role=\"alert\"` inside the cell, which screen readers read on insertion —\n * announcing it again from the shared region would read it twice and put the\n * same text in the document twice.\n */\n const persist = useCallback(\n async (id: string, column: DataTableColumn<T>, change: DataTableCellChange<T>) => {\n setSaving((current) => ({ ...current, [id]: true }));\n try {\n await onCellChange?.(change);\n announce(labels.saved(headerText(column)));\n } catch (error) {\n setOverrides((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n const message =\n error instanceof Error && error.message\n ? error.message\n : labels.saveFailed(headerText(column));\n setErrors((current) => ({ ...current, [id]: message }));\n } finally {\n setSaving((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }\n },\n [onCellChange, announce, labels],\n );\n\n const moveFrom = useCallback(\n (id: string, move: CellCommitMove): void => {\n if (move === \"none\") {\n setEditing(null);\n setRefocus(id);\n return;\n }\n const index = editableCellIds.indexOf(id);\n const target = editableCellIds[index + (move === \"next\" ? 1 : -1)];\n if (target === undefined) {\n setEditing(null);\n setRefocus(id);\n return;\n }\n setEditing(target);\n setRefocus(null);\n },\n [editableCellIds],\n );\n\n /**\n * Parse, validate, stage optimistically, then save in the background.\n *\n * Validation runs before anything is staged, and a rejection leaves the editor\n * open with the message attached to the input — the user has to be able to fix\n * what they typed without retyping it.\n */\n const commit = useCallback(\n (\n row: T,\n column: DataTableColumn<T>,\n id: string,\n pageIndex: number,\n raw: string,\n move: CellCommitMove,\n ): void => {\n const previous = displayed(row, column, id);\n const currentText = column.formatEdit ? column.formatEdit(row) : String(previous ?? \"\");\n if (raw === currentText) {\n moveFrom(id, move);\n return;\n }\n\n const value = column.parse\n ? column.parse(raw, row)\n : column.editorType === \"number\"\n ? Number(raw)\n : raw.trim();\n\n const invalid = column.validate?.(value, row) ?? null;\n if (invalid) {\n setErrors((current) => ({ ...current, [id]: invalid }));\n return;\n }\n\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n setOverrides((current) => ({ ...current, [id]: value }));\n moveFrom(id, move);\n void persist(id, column, {\n row,\n key: column.key,\n value,\n previous,\n rowIndex: absoluteIndex(pageIndex),\n });\n },\n [displayed, moveFrom, persist, absoluteIndex],\n );\n\n const tableColumns = useMemo<TableColumn<T>[]>(\n () =>\n columns.map((column) => {\n const isSorted = sort?.key === column.key;\n const indicator = isSorted ? (sort?.direction === \"asc\" ? \" ▲\" : \" ▼\") : \"\";\n const header = column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n aria-label={`Ordenar por ${headerText(column)}`}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {indicator}\n </span>\n </button>\n ) : (\n column.header\n );\n\n const plainCell = (row: T): ReactNode => {\n if (column.render) return column.render(row);\n return (row[column.key] as ReactNode) ?? null;\n };\n\n /**\n * Render an editable cell's content against the optimistic value.\n *\n * The row is shallow-patched rather than the value passed alongside it,\n * because a column with a custom `render` (a `<Money>`, a badge) reads\n * the row — handing it the stale row would show the old number under a\n * cell the user just changed.\n */\n const patchedCell = (row: T, value: unknown): ReactNode => {\n const patched = { ...row, [column.key]: value } as T;\n if (column.render) return column.render(patched);\n return (patched[column.key] as ReactNode) ?? null;\n };\n\n const editable = editingEnabled && column.editable === true;\n\n return {\n key: String(column.key),\n header,\n align: column.align,\n priority: column.priority,\n width: column.width,\n render: editable\n ? (row: T, index: number) => {\n const id = cellId(rowKey(row, absoluteIndex(index)), column.key);\n const value = displayed(row, column, id);\n const text = column.formatEdit\n ? column.formatEdit(row)\n : String(value ?? \"\");\n return (\n <EditableCell\n text={text}\n columnLabel={headerText(column)}\n rowNumber={index + 1}\n inputType={column.editorType ?? \"text\"}\n editing={editing === id}\n refocus={refocus === id}\n saving={saving[id] === true}\n error={errors[id] ?? null}\n errorId={`tempest-cell-error-${id.replace(/[^\\w-]/g, \"_\")}`}\n labels={labels}\n onOpen={() => {\n setEditing(id);\n setRefocus(null);\n }}\n onCommit={(raw, move) =>\n commit(row, column, id, index, raw, move)\n }\n onCancel={() => {\n setEditing(null);\n setRefocus(id);\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }}\n >\n {patchedCell(row, value)}\n </EditableCell>\n );\n }\n : plainCell,\n };\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n columns,\n sort,\n editingEnabled,\n editing,\n refocus,\n saving,\n errors,\n labels,\n overrides,\n absoluteIndex,\n commit,\n ],\n );\n\n const showSkeleton = loading && pageRows.length === 0;\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {searchable && (\n <SearchBar\n value={search}\n onChange={(value) => {\n setSearch(value);\n setPage(1);\n onSearchChange?.(value);\n }}\n wrapperClassName={styles.search}\n />\n )}\n <div\n className={cn(loading && !showSkeleton && styles.pending)}\n aria-busy={loading || undefined}\n data-testid=\"tempest-datatable-body\"\n >\n {showSkeleton ? (\n <LoadingRows columns={tableColumns.length} rows={Math.min(pageSize, 8)} />\n ) : (\n <Table\n columns={tableColumns}\n data={pageRows}\n rowKey={(row, index) => rowKey(row, absoluteIndex(index))}\n emptyMessage={emptyMessage}\n />\n )}\n </div>\n {totalPages > 1 && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n onPageChange={setPage}\n totalItems={rowCount}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the table does four jobs\n * a caller turns on independently — paging (pageSize), search (searchable,\n * searchKeys), sort (initialSort) and inline edit (onCellChange, editLabels) — over\n * one row model (data, columns, rowKey, emptyMessage). The body is long because\n * those four share the derived-rows pipeline: filter, then sort, then page, then map\n * to cells, in that order and off the same memo.\n *\n * Each of the four also runs in a second mode, where the caller owns the work and\n * the table only reports intent: totalItems/page/onPageChange, onSearchChange,\n * manualSort/onSortChange, loading/loadingRows. That doubles the props without\n * adding a fifth job — every manual prop short-circuits one stage of the same\n * pipeline.\n */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\nimport { usePagination } from \"@/hooks\";\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { Table, type TableAlign, type TableColumn, type TablePriority } from \"../Table\";\nimport { Pagination } from \"../Pagination\";\nimport { SearchBar } from \"../SearchBar\";\nimport { EditableCell } from \"./EditableCell\";\nimport { LoadingRows } from \"./LoadingRows\";\nimport { useDevWarnings } from \"./use-dev-warnings\";\nimport { DEFAULT_EDIT_LABELS, type CellCommitMove, type DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort<T> {\n key: keyof T;\n direction: SortDirection;\n}\n\n/** Input types an editable column can use. */\nexport type DataTableEditorType = \"text\" | \"number\" | \"date\" | \"email\" | \"tel\" | \"url\";\n\n/** One accepted cell edit, handed to `onCellChange`. */\nexport interface DataTableCellChange<T> {\n /** The row as it was before the edit. */\n row: T;\n /** Which column changed. */\n key: keyof T;\n /** The parsed new value. */\n value: unknown;\n /** The value that was displayed before the edit. */\n previous: unknown;\n /** Index of the row in the full `data` array. */\n rowIndex: number;\n}\n\n/**\n * Column definition for {@link DataTable}. Extends the headless {@link Table}\n * column shape with a typed `key`, opt-in sorting, opt-in inline editing, and the\n * visual options that are forwarded to the underlying Table cell.\n */\nexport interface DataTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `String(row[key])`. */\n render?: (row: T) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment forwarded to the Table cell. */\n align?: TableAlign;\n /** Responsive visibility priority forwarded to the Table cell. */\n priority?: TablePriority;\n /** Fixed column width forwarded to the Table cell. */\n width?: string | number;\n /**\n * Let cells in this column be edited in place. Requires `onCellChange` on the\n * table; without it the column stays read-only.\n */\n editable?: boolean;\n /** Editor input type. Default `\"text\"`. */\n editorType?: DataTableEditorType;\n /** Text the editor opens with. Defaults to `String(value ?? \"\")`. */\n formatEdit?: (row: T) => string;\n /**\n * Turn the typed string into the stored value. Defaults to the trimmed string,\n * or `Number(raw)` when `editorType` is `\"number\"`.\n */\n parse?: (raw: string, row: T) => unknown;\n /** Return a message to reject the edit, or `null` to accept it. */\n validate?: (value: unknown, row: T) => string | null;\n}\n\n/** Everything a table needs regardless of who owns paging, sorting and searching. */\nexport interface DataTableBaseProps<T> extends HTMLAttributes<HTMLDivElement> {\n /**\n * The rows to work with.\n *\n * By default this is the **full** dataset and sorting, searching and paging\n * all happen in memory. Pass `totalItems` and it becomes the current page as\n * the server returned it, with those three delegated to the caller.\n */\n data: T[];\n /** Column definitions. */\n columns: DataTableColumn<T>[];\n /** Rows per page. Default 10. */\n pageSize?: number;\n /** Render a search input above the table. Default false. */\n searchable?: boolean;\n /**\n * Keys to match the search term against. When omitted, every column whose\n * value is a string or number is searched.\n */\n searchKeys?: (keyof T)[];\n /** Initial sort applied before any header interaction. */\n initialSort?: DataTableSort<T>;\n /** Stable key extractor for rows. Defaults to the row index. */\n rowKey?: (row: T, index: number) => string | number;\n /** Content shown when no rows match. */\n emptyMessage?: ReactNode;\n /**\n * Persist an accepted cell edit. Return a promise: while it is pending the cell\n * already shows the new value, and a rejection rolls that back and surfaces the\n * error in the cell. Without this prop no column is editable.\n */\n onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;\n /** Override the PT-BR copy of the editing affordances. */\n editLabels?: Partial<DataTableEditLabels>;\n /**\n * Searching is the caller's job: typing reports through `onSearchChange` and\n * the rows are left as they arrived.\n *\n * Implied by `totalItems`. Filtering the current page would hide the rows\n * that do not match *on this page* and show nothing for a term that only\n * matches on page three — an empty table that looks like \"no results\".\n */\n manualSearch?: boolean;\n /** Called with the current search term (debouncing, if any, is the caller's). */\n onSearchChange?: (term: string) => void;\n /**\n * A fetch is in flight.\n *\n * With rows already on screen they stay put, dimmed and `aria-busy`, so the\n * page does not jump under the cursor between pages. With no rows yet it\n * renders placeholder lines at full height, which is a different statement\n * from `emptyMessage`: \"loading\" and \"there is nothing\" are not the same\n * screen.\n */\n loading?: boolean;\n}\n\n/**\n * Paging, as one of the three shapes that actually work.\n *\n * These used to be three optional props, so the compiler accepted\n * `totalItems` with no `page` — a table whose pager moves an internal page while\n * `data` keeps showing page one. Every prop was optional on its own, so the only\n * place left to catch it was a `console.warn` in dev, in the browser, with the\n * component mounted. As a union the same mistake is a build error at the call\n * site, for free, everywhere.\n */\nexport type DataTablePagingProps =\n | {\n /** Not server mode. */\n totalItems?: never;\n /** The table owns the page. */\n page?: never;\n /** Nothing to report to. */\n onPageChange?: never;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). Pair it with\n * `page` and `onPageChange`.\n */\n totalItems?: never;\n /** Current page, 1-based. Controlled — required in server mode. */\n page: number;\n /** Called with the next page. Required whenever `page` is controlled. */\n onPageChange: (page: number) => void;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). `page` and\n * `onPageChange` come with it — the type says so, because a server-mode\n * table without them silently shows page one forever.\n */\n totalItems: number;\n /** Current page, 1-based. Controlled, and required in server mode. */\n page: number;\n /** Called with the next page. */\n onPageChange: (page: number) => void;\n };\n\n/**\n * Sorting: delegated, and therefore reported, or neither.\n *\n * `manualSort` without `onSortChange` renders a header that moves its arrow and\n * changes nothing else — the arrow is a lie the compiler can now catch.\n */\nexport type DataTableSortProps<T> =\n | {\n /** The table sorts the rows it has. */\n manualSort?: false;\n /** Called with the next sort state — `null` when the header cycles back to unsorted. */\n onSortChange?: (sort: DataTableSort<T> | null) => void;\n }\n | {\n /**\n * Sorting is the caller's job: clicking a header reports through\n * `onSortChange` and the rows are left in the order they arrived.\n *\n * Implied by `totalItems`, because sorting the page in memory would sort\n * *that page only* while the header claims the whole table is ordered.\n */\n manualSort: true;\n /** Where the click goes. Required, since nothing else acts on it. */\n onSortChange: (sort: DataTableSort<T> | null) => void;\n };\n\n/**\n * The table's props: the shared half, plus one valid paging shape and one valid\n * sorting shape.\n */\nexport type DataTableProps<T> = DataTableBaseProps<T> &\n DataTablePagingProps &\n DataTableSortProps<T>;\n\n/** Identity of one cell, stable across re-renders and pagination. */\nfunction cellId(rowKeyValue: string | number, columnKey: PropertyKey): string {\n return `${String(rowKeyValue)}::${String(columnKey)}`;\n}\n\nfunction headerText<T>(column: DataTableColumn<T>): string {\n return typeof column.header === \"string\" ? column.header : String(column.key);\n}\n\n/**\n * Stateful, headless data table built on top of {@link Table}. Adds\n * client-side searching, click-to-sort columns, pagination and opt-in inline\n * editing while delegating all table markup to the underlying Table component.\n *\n * - Clicking a sortable header cycles asc → desc → unsorted.\n * - Search matches a case-insensitive substring across `searchKeys`\n * (or every string/number column when not provided).\n * - Pagination is hidden when the result fits on a single page.\n * - A column with `editable` renders a button that opens an inline editor;\n * `Enter` commits, `Escape` discards, `Tab` walks to the next editable cell.\n *\n * Editing is strictly opt-in: with no `editable` column (or no `onCellChange`) the\n * rendered markup is byte-for-byte what it was before the feature existed, which\n * matters because the component is published.\n *\n * ## Optimistic, with a visible rollback\n *\n * An accepted edit is shown immediately and `onCellChange` runs in the background.\n * If it rejects, the cell returns to the old value **and** shows the reason as a\n * `role=\"alert\"` tied to the cell. A silent revert is worse than no optimistic\n * update at all: the user watched their edit appear and has no reason to doubt it.\n *\n * The header memo depends on `columns`, `sort` and the editing state only:\n * `toggleSort` and the commit callbacks are recreated each render but always close\n * over the same setters, so including them would rebuild every header on every\n * render without changing behaviour. That is why `exhaustive-deps` is silenced on\n * that dependency array.\n */\nexport function DataTable<T>({\n data,\n columns,\n pageSize = 10,\n searchable = false,\n searchKeys,\n initialSort,\n rowKey = (_row, index) => index,\n emptyMessage,\n onCellChange,\n editLabels,\n totalItems,\n page: controlledPage,\n onPageChange,\n manualSort,\n onSortChange,\n manualSearch,\n onSearchChange,\n loading = false,\n className,\n ...rest\n}: DataTableProps<T>) {\n const [search, setSearch] = useState<string>(\"\");\n const [sort, setSort] = useState<DataTableSort<T> | null>(initialSort ?? null);\n const { page: internalPage, setPage: setInternalPage } = usePagination(1, pageSize);\n const announce = useAnnounce();\n\n const serverMode = totalItems !== undefined;\n const sortIsManual = manualSort ?? serverMode;\n const searchIsManual = manualSearch ?? serverMode;\n const page = controlledPage ?? internalPage;\n\n const setPage = useCallback(\n (next: number) => {\n if (controlledPage === undefined) setInternalPage(next);\n onPageChange?.(next);\n },\n [controlledPage, setInternalPage, onPageChange],\n );\n\n useDevWarnings({ serverMode, controlledPage, onPageChange, sortIsManual, onSortChange });\n\n const [editing, setEditing] = useState<string | null>(null);\n const [refocus, setRefocus] = useState<string | null>(null);\n const [overrides, setOverrides] = useState<Record<string, unknown>>({});\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [saving, setSaving] = useState<Record<string, boolean>>({});\n\n const labels = useMemo<DataTableEditLabels>(\n () => ({ ...DEFAULT_EDIT_LABELS, ...editLabels }),\n [editLabels],\n );\n const editingEnabled = onCellChange !== undefined && columns.some((column) => column.editable);\n\n const effectiveSearchKeys = useMemo<(keyof T)[]>(() => {\n if (!searchable || searchIsManual) return [];\n if (searchKeys && searchKeys.length > 0) return searchKeys;\n return columns\n .filter((column) => {\n const sample = data.find((row) => row[column.key] != null);\n const value = sample ? sample[column.key] : undefined;\n return typeof value === \"string\" || typeof value === \"number\";\n })\n .map((column) => column.key);\n }, [searchable, searchIsManual, searchKeys, columns, data]);\n\n const filtered = useMemo<T[]>(() => {\n const term = search.trim().toLowerCase();\n if (!term || !searchable || searchIsManual) return data;\n return data.filter((row) =>\n effectiveSearchKeys.some((key) => {\n const value = row[key];\n return value != null && String(value).toLowerCase().includes(term);\n }),\n );\n }, [data, search, searchable, searchIsManual, effectiveSearchKeys]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort || sortIsManual) return filtered;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...filtered].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [filtered, sort, sortIsManual]);\n\n const rowCount = totalItems ?? sorted.length;\n const totalPages = Math.max(1, Math.ceil(rowCount / pageSize));\n\n /**\n * Clamp the current page when the dataset shrinks (e.g. after filtering).\n *\n * Skipped in server mode: `page` belongs to the caller there, and a clamp\n * fired against a `totalItems` that has not caught up with the new filter\n * yet would send them a page they did not ask for, mid-fetch.\n */\n useEffect(() => {\n if (!serverMode && page > totalPages) setPage(totalPages);\n }, [serverMode, page, totalPages, setPage]);\n\n const safePage = serverMode ? page : Math.min(page, totalPages);\n const pageRows = useMemo<T[]>(() => {\n if (serverMode) return sorted;\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [serverMode, sorted, safePage, pageSize]);\n\n function toggleSort(key: keyof T): void {\n const current = sort;\n const next: DataTableSort<T> | null =\n !current || current.key !== key\n ? { key, direction: \"asc\" }\n : current.direction === \"asc\"\n ? { key, direction: \"desc\" }\n : null;\n setSort(next);\n onSortChange?.(next);\n }\n\n const absoluteIndex = useCallback(\n (pageIndex: number) => (safePage - 1) * pageSize + pageIndex,\n [safePage, pageSize],\n );\n\n /**\n * Every editable cell on the page, row-major — the order `Tab` walks.\n *\n * Row-major and not column-major because a row is the record a user is\n * correcting; walking down a column would make them re-find their place on\n * every keystroke.\n */\n const editableCellIds = useMemo<string[]>(() => {\n if (!editingEnabled) return [];\n const ids: string[] = [];\n pageRows.forEach((row, index) => {\n const key = rowKey(row, absoluteIndex(index));\n for (const column of columns) {\n if (column.editable) ids.push(cellId(key, column.key));\n }\n });\n return ids;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editingEnabled, pageRows, columns, absoluteIndex]);\n\n const displayed = useCallback(\n (row: T, column: DataTableColumn<T>, id: string): unknown =>\n id in overrides ? overrides[id] : row[column.key],\n [overrides],\n );\n\n /**\n * Run the caller's `onCellChange` behind the optimistic update.\n *\n * Only the *success* is announced. The failure already renders as a\n * `role=\"alert\"` inside the cell, which screen readers read on insertion —\n * announcing it again from the shared region would read it twice and put the\n * same text in the document twice.\n */\n const persist = useCallback(\n async (id: string, column: DataTableColumn<T>, change: DataTableCellChange<T>) => {\n setSaving((current) => ({ ...current, [id]: true }));\n try {\n await onCellChange?.(change);\n announce(labels.saved(headerText(column)));\n } catch (error) {\n setOverrides((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n const message =\n error instanceof Error && error.message\n ? error.message\n : labels.saveFailed(headerText(column));\n setErrors((current) => ({ ...current, [id]: message }));\n } finally {\n setSaving((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }\n },\n [onCellChange, announce, labels],\n );\n\n const moveFrom = useCallback(\n (id: string, move: CellCommitMove): void => {\n if (move === \"none\") {\n setEditing(null);\n setRefocus(id);\n return;\n }\n const index = editableCellIds.indexOf(id);\n const target = editableCellIds[index + (move === \"next\" ? 1 : -1)];\n if (target === undefined) {\n setEditing(null);\n setRefocus(id);\n return;\n }\n setEditing(target);\n setRefocus(null);\n },\n [editableCellIds],\n );\n\n /**\n * Parse, validate, stage optimistically, then save in the background.\n *\n * Validation runs before anything is staged, and a rejection leaves the editor\n * open with the message attached to the input — the user has to be able to fix\n * what they typed without retyping it.\n */\n const commit = useCallback(\n (\n row: T,\n column: DataTableColumn<T>,\n id: string,\n pageIndex: number,\n raw: string,\n move: CellCommitMove,\n ): void => {\n const previous = displayed(row, column, id);\n const currentText = column.formatEdit ? column.formatEdit(row) : String(previous ?? \"\");\n if (raw === currentText) {\n moveFrom(id, move);\n return;\n }\n\n const value = column.parse\n ? column.parse(raw, row)\n : column.editorType === \"number\"\n ? Number(raw)\n : raw.trim();\n\n const invalid = column.validate?.(value, row) ?? null;\n if (invalid) {\n setErrors((current) => ({ ...current, [id]: invalid }));\n return;\n }\n\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n setOverrides((current) => ({ ...current, [id]: value }));\n moveFrom(id, move);\n void persist(id, column, {\n row,\n key: column.key,\n value,\n previous,\n rowIndex: absoluteIndex(pageIndex),\n });\n },\n [displayed, moveFrom, persist, absoluteIndex],\n );\n\n const tableColumns = useMemo<TableColumn<T>[]>(\n () =>\n columns.map((column) => {\n const isSorted = sort?.key === column.key;\n const indicator = isSorted ? (sort?.direction === \"asc\" ? \" ▲\" : \" ▼\") : \"\";\n const header = column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n aria-label={`Ordenar por ${headerText(column)}`}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {indicator}\n </span>\n </button>\n ) : (\n column.header\n );\n\n const plainCell = (row: T): ReactNode => {\n if (column.render) return column.render(row);\n return (row[column.key] as ReactNode) ?? null;\n };\n\n /**\n * Render an editable cell's content against the optimistic value.\n *\n * The row is shallow-patched rather than the value passed alongside it,\n * because a column with a custom `render` (a `<Money>`, a badge) reads\n * the row — handing it the stale row would show the old number under a\n * cell the user just changed.\n */\n const patchedCell = (row: T, value: unknown): ReactNode => {\n const patched = { ...row, [column.key]: value } as T;\n if (column.render) return column.render(patched);\n return (patched[column.key] as ReactNode) ?? null;\n };\n\n const editable = editingEnabled && column.editable === true;\n\n return {\n key: String(column.key),\n header,\n align: column.align,\n priority: column.priority,\n width: column.width,\n render: editable\n ? (row: T, index: number) => {\n const id = cellId(rowKey(row, absoluteIndex(index)), column.key);\n const value = displayed(row, column, id);\n const text = column.formatEdit\n ? column.formatEdit(row)\n : String(value ?? \"\");\n return (\n <EditableCell\n text={text}\n columnLabel={headerText(column)}\n rowNumber={index + 1}\n inputType={column.editorType ?? \"text\"}\n editing={editing === id}\n refocus={refocus === id}\n saving={saving[id] === true}\n error={errors[id] ?? null}\n errorId={`tempest-cell-error-${id.replace(/[^\\w-]/g, \"_\")}`}\n labels={labels}\n onOpen={() => {\n setEditing(id);\n setRefocus(null);\n }}\n onCommit={(raw, move) =>\n commit(row, column, id, index, raw, move)\n }\n onCancel={() => {\n setEditing(null);\n setRefocus(id);\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }}\n >\n {patchedCell(row, value)}\n </EditableCell>\n );\n }\n : plainCell,\n };\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n columns,\n sort,\n editingEnabled,\n editing,\n refocus,\n saving,\n errors,\n labels,\n overrides,\n absoluteIndex,\n commit,\n ],\n );\n\n const showSkeleton = loading && pageRows.length === 0;\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {searchable && (\n <SearchBar\n value={search}\n onChange={(value) => {\n setSearch(value);\n setPage(1);\n onSearchChange?.(value);\n }}\n wrapperClassName={styles.search}\n />\n )}\n <div\n className={cn(loading && !showSkeleton && styles.pending)}\n aria-busy={loading || undefined}\n data-testid=\"tempest-datatable-body\"\n >\n {showSkeleton ? (\n <LoadingRows columns={tableColumns.length} rows={Math.min(pageSize, 8)} />\n ) : (\n <Table\n columns={tableColumns}\n data={pageRows}\n rowKey={(row, index) => rowKey(row, absoluteIndex(index))}\n emptyMessage={emptyMessage}\n />\n )}\n </div>\n {totalPages > 1 && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n onPageChange={setPage}\n totalItems={rowCount}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./filter-model.cjs");var
|
|
1
|
+
const e=require("./filter-model.cjs"),t=require("../../utils/format.cjs");var n=/^\d{4}-\d{2}-\d{2}$/,r=new Intl.Collator(void 0,{numeric:!0});function i(e,t){if(!(typeof e!=`object`||!e))return e[t]}function a(e){return e instanceof Date?t.formatDateForInput(e)||null:typeof e!=`string`||e.trim()===``?null:t.formatDateForInput(e)||null}function o(e,t){if(e==null)return null;if(typeof e==`number`){let n=Number(t);return Number.isNaN(n)?null:e-n}if(typeof e==`boolean`){let n=t===`true`||t!==`false`&&null;return n===null?null:Number(e)-Number(n)}if(n.test(t)){let n=a(e);return n===null?null:n.localeCompare(t)}return r.compare(String(e),t)}function s(e,t){if(e===t)return!0;let n=o(e,t);return n===null?String(e??``)===t:n===0}function c(e){return e==null?!0:typeof e==`string`?e.trim()===``:Array.isArray(e)?e.length===0:!1}function l(e){return e.value===void 0?[]:Array.isArray(e.value)?[...e.value]:[String(e.value)]}function u(e,t,n){switch(e){case`empty`:return c(t);case`notEmpty`:return!c(t);case`eq`:return s(t,n[0]??``);case`ne`:return!s(t,n[0]??``);case`contains`:return String(t??``).toLowerCase().includes((n[0]??``).toLowerCase());case`in`:return n.some(e=>s(t,e));case`between`:{let[e=``,r=``]=n,i=o(t,e),a=o(t,r);return i===null||a===null?!1:i>=0&&a<=0}default:{let r=o(t,n[0]??``);return r===null?!1:e===`gt`?r>0:e===`gte`?r>=0:e===`lt`?r<0:r<=0}}}function d(e){let[t=``,i=``]=e;if(n.test(t)&&n.test(i))return t<=i?[t,i]:[i,t];let a=Number(t),o=Number(i);return!Number.isNaN(a)&&!Number.isNaN(o)?a<=o?[t,i]:[i,t]:r.compare(t,i)<=0?[t,i]:[i,t]}function f(t,n){let r=n.filter(e.isComplete).map(e=>({field:e.field,operator:e.operator,values:e.operator===`between`?d(l(e)):l(e)}));return r.length===0?[...t]:t.filter(e=>r.every(({field:t,operator:n,values:r})=>u(n,i(e,t),r)))}exports.applyFilters=f,exports.orderRange=d;
|
|
2
2
|
//# sourceMappingURL=filter-apply.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filter-apply.cjs","names":[],"sources":["../../../src/components/FilterBar/filter-apply.ts"],"sourcesContent":["// The other half of the filter model: evaluating a `Filter[]` instead of only\n// describing it. `FilterBar` produces filters, `applyFilters` runs them over an\n// in-memory list, and `filtersToQueryParams` (filter-query.ts) hands the same set\n// to a paginated backend. Without these two the model stops one step short of\n// useful and every app writes the eleven operator branches again.\n\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete, isValueless } from \"./filter-model\";\n\n/** Matches a plain `yyyy-mm-dd`, the shape `<input type=\"date\">` produces. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * Read a property off a row without asserting the row's shape.\n *\n * `Filter.field` is a string chosen by the app's field list, so it cannot be a\n * `keyof T`; the lookup goes through an index signature and a missing property\n * yields `undefined`, which the operators treat as an empty value.\n *\n * @param item - The row being tested.\n * @param field - Property name from the filter.\n * @returns The property value, or `undefined` when the row has no such key.\n */\nfunction readField(item: unknown, field: string): unknown {\n if (item == null || typeof item !== \"object\") return undefined;\n return (item as Record<string, unknown>)[field];\n}\n\n/**\n * Local `yyyy-mm-dd` for a value that represents a date, or `null`.\n *\n * Built from the local calendar parts rather than `toISOString`, which converts\n * to UTC first and therefore reports the next day for anything after 21:00 in\n * UTC-3. A `yyyy-mm-dd` string is returned untouched, so a filter value and a\n * row value that already agree never round-trip through `Date` at all.\n *\n * @param value - A `Date`, a date-ish string, or anything else.\n * @returns The day key, or `null` when the value does not denote a date.\n */\nfunction dayKey(value: unknown): string | null {\n if (typeof value === \"string\" && DATE_ONLY.test(value)) return value;\n\n const date =\n value instanceof Date\n ? value\n : typeof value === \"string\" && value.trim() !== \"\"\n ? new Date(value)\n : null;\n if (date === null || Number.isNaN(date.getTime())) return null;\n\n const month = `${date.getMonth() + 1}`.padStart(2, \"0\");\n const day = `${date.getDate()}`.padStart(2, \"0\");\n return `${date.getFullYear()}-${month}-${day}`;\n}\n\n/**\n * Compare a row value against a filter value, in the row value's own type.\n *\n * The filter always carries strings — it comes from form inputs and from the\n * URL — while the row carries whatever the API returned. Comparing them as text\n * gets the two common cases wrong: `10` would sort before `9`, and a date would\n * be compared by however it happens to be formatted. So the row value picks the\n * comparison, and the filter value is coerced into it.\n *\n * Dates compare by **day**, not by instant: a row stamped `2026-03-05T13:00:00Z`\n * is `>= 2026-03-05`, which is what the person picking a date in the UI means.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Negative, zero or positive, or `null` when the two cannot be compared.\n */\nfunction compare(left: unknown, right: string): number | null {\n if (left == null) return null;\n\n if (typeof left === \"number\") {\n const parsed = Number(right);\n return Number.isNaN(parsed) ? null : left - parsed;\n }\n\n if (typeof left === \"boolean\") {\n const parsed = right === \"true\" ? true : right === \"false\" ? false : null;\n return parsed === null ? null : Number(left) - Number(parsed);\n }\n\n if (DATE_ONLY.test(right)) {\n const key = dayKey(left);\n return key === null ? null : key.localeCompare(right);\n }\n\n return String(left).localeCompare(right, undefined, { numeric: true });\n}\n\n/**\n * Equality in the row value's type, falling back to exact text.\n *\n * Case-sensitive on purpose: `eq` maps to a plain `WHERE column = value` on the\n * server side, so a case-insensitive client would quietly disagree with the\n * backend for the same filter. `contains` is the case-insensitive operator, and\n * it is also the default one for text fields, so the friendly behaviour is what\n * people get without asking.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Whether the two are equal.\n */\nfunction equals(left: unknown, right: string): boolean {\n const result = compare(left, right);\n if (result !== null) return result === 0;\n return String(left ?? \"\") === right;\n}\n\n/**\n * Is this row value empty?\n *\n * Empty means absent, blank text or an empty list — never falsy. `0` and `false`\n * are values somebody chose, and a filter that hid them would be reporting the\n * wrong count on every dashboard that tracks zeroes.\n *\n * @param value - The row value.\n * @returns Whether the value counts as empty.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * The filter's value as a list, whatever arity the operator uses.\n *\n * @param filter - The filter to read.\n * @returns Its values, trimmed of nothing — a leading space may be meaningful text.\n */\nfunction valuesOf(filter: Filter): string[] {\n if (filter.value === undefined) return [];\n return Array.isArray(filter.value) ? [...filter.value] : [String(filter.value)];\n}\n\n/**\n * Evaluate one operator against one row value.\n *\n * @param operator - The comparison to make.\n * @param left - The row value.\n * @param values - The filter values (one, two for `between`, many for `in`).\n * @returns Whether the row value satisfies the operator.\n */\nfunction matchesOperator(operator: FilterOperator, left: unknown, values: string[]): boolean {\n switch (operator) {\n case \"empty\":\n return isEmptyValue(left);\n case \"notEmpty\":\n return !isEmptyValue(left);\n case \"eq\":\n return equals(left, values[0] ?? \"\");\n case \"ne\":\n return !equals(left, values[0] ?? \"\");\n case \"contains\":\n return String(left ?? \"\")\n .toLowerCase()\n .includes((values[0] ?? \"\").toLowerCase());\n case \"in\":\n return values.some((value) => equals(left, value));\n case \"between\": {\n const [first = \"\", second = \"\"] = values;\n const low = compare(left, first);\n const high = compare(left, second);\n if (low === null || high === null) return false;\n return low >= 0 && high <= 0;\n }\n default: {\n const result = compare(left, values[0] ?? \"\");\n if (result === null) return false;\n if (operator === \"gt\") return result > 0;\n if (operator === \"gte\") return result >= 0;\n if (operator === \"lt\") return result < 0;\n return result <= 0;\n }\n }\n}\n\n/**\n * Order the two values of a `between` so the range is never empty by accident.\n *\n * A person who picks the later date first means the range between the two dates,\n * not \"no rows\". The server-side `BETWEEN` needs `(lo, hi)` in order too, which\n * is why the normalisation lives here and is shared by `filtersToQueryParams`.\n *\n * @param values - The raw pair.\n * @returns The pair, ascending.\n */\nexport function orderRange(values: readonly string[]): string[] {\n const [first = \"\", second = \"\"] = values;\n if (DATE_ONLY.test(first) && DATE_ONLY.test(second)) {\n return first <= second ? [first, second] : [second, first];\n }\n const low = Number(first);\n const high = Number(second);\n if (!Number.isNaN(low) && !Number.isNaN(high)) {\n return low <= high ? [first, second] : [second, first];\n }\n return first.localeCompare(second, undefined, { numeric: true }) <= 0\n ? [first, second]\n : [second, first];\n}\n\n/**\n * Run a filter set over an in-memory list.\n *\n * Closes the loop `FilterBar` opens: the bar produces `Filter[]`, this applies\n * them. Filters combine with `AND`, matching the flat model the bar builds, and\n * an incomplete filter is skipped rather than treated as a match of nothing — a\n * half-filled form should not empty the table underneath it.\n *\n * Comparison follows the row's type, not the filter's: numbers compare\n * numerically, dates compare by day, and everything else compares as text with\n * `numeric: true` so `\"item 2\"` lands before `\"item 10\"`.\n *\n * A few behaviours differ from the SQL the server-side twin produces, and the\n * difference is deliberate rather than accidental:\n *\n * - `ne` matches rows whose value is absent. In SQL, `column <> 'x'` is `NULL`\n * for a `NULL` column and the row drops out. Here \"is not paid\" shows the rows\n * with no status at all, which is what the chip claims.\n * - `empty` matches `NULL`, blank text and empty lists; `__isnull` on the server\n * only matches `NULL`. A column that stores `\"\"` instead of `NULL` is where\n * the two disagree.\n *\n * @example\n * const visible = applyFilters(orders, filters);\n *\n * @param items - The full list.\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns A new array with the rows that satisfy every complete filter.\n */\nexport function applyFilters<T>(items: readonly T[], filters: readonly Filter[]): T[] {\n const active = filters.filter(isComplete);\n if (active.length === 0) return [...items];\n\n return items.filter((item) =>\n active.every((filter) => {\n const left = readField(item, filter.field);\n if (isValueless(filter.operator)) {\n return matchesOperator(filter.operator, left, []);\n }\n const values =\n filter.operator === \"between\" ? orderRange(valuesOf(filter)) : valuesOf(filter);\n return matchesOperator(filter.operator, left, values);\n }),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filter-apply.cjs","names":[],"sources":["../../../src/components/FilterBar/filter-apply.ts"],"sourcesContent":["// The other half of the filter model: evaluating a `Filter[]` instead of only\n// describing it. `FilterBar` produces filters, `applyFilters` runs them over an\n// in-memory list, and `filtersToQueryParams` (filter-query.ts) hands the same set\n// to a paginated backend. Without these two the model stops one step short of\n// useful and every app writes the eleven operator branches again.\n\nimport { formatDateForInput } from \"@/utils/format\";\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete } from \"./filter-model\";\n\n/** Matches a plain `yyyy-mm-dd`, the shape `<input type=\"date\">` produces. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * One collator for every text comparison in this module.\n *\n * `localeCompare(value, undefined, { numeric: true })` builds a collator on each\n * call — passing an options bag opts out of the engine's cached-default fast\n * path — and this comparison runs once per row per filter. Measured at 200k\n * comparisons: 453 ms through `localeCompare` against 25 ms through a hoisted\n * collator, i.e. ~23 ms of main thread per keystroke on a 10k-row list.\n */\nconst TEXT_COLLATOR = new Intl.Collator(undefined, { numeric: true });\n\n/**\n * Read a property off a row without asserting the row's shape.\n *\n * `Filter.field` is a string chosen by the app's field list, so it cannot be a\n * `keyof T`; the lookup goes through an index signature and a missing property\n * yields `undefined`, which the operators treat as an empty value.\n *\n * @param item - The row being tested.\n * @param field - Property name from the filter.\n * @returns The property value, or `undefined` when the row has no such key.\n */\nfunction readField(item: unknown, field: string): unknown {\n if (item == null || typeof item !== \"object\") return undefined;\n return (item as Record<string, unknown>)[field];\n}\n\n/**\n * Local `yyyy-mm-dd` for a value that represents a date, or `null`.\n *\n * Delegates the formatting to {@link formatDateForInput}, which owns the rule\n * that the key is built from the local calendar parts rather than `toISOString`.\n * This adds only the narrowing from `unknown` and maps \"not a date\" to `null`\n * instead of to the empty string a date input wants.\n *\n * @param value - A `Date`, a date-ish string, or anything else.\n * @returns The day key, or `null` when the value does not denote a date.\n */\nfunction dayKey(value: unknown): string | null {\n if (value instanceof Date) return formatDateForInput(value) || null;\n if (typeof value !== \"string\" || value.trim() === \"\") return null;\n return formatDateForInput(value) || null;\n}\n\n/**\n * Compare a row value against a filter value, in the row value's own type.\n *\n * The filter always carries strings — it comes from form inputs and from the\n * URL — while the row carries whatever the API returned. Comparing them as text\n * gets the two common cases wrong: `10` would sort before `9`, and a date would\n * be compared by however it happens to be formatted. So the row value picks the\n * comparison, and the filter value is coerced into it.\n *\n * Dates compare by **day**, not by instant: a row stamped `2026-03-05T13:00:00Z`\n * is `>= 2026-03-05`, which is what the person picking a date in the UI means.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Negative, zero or positive, or `null` when the two cannot be compared.\n */\nfunction compare(left: unknown, right: string): number | null {\n if (left == null) return null;\n\n if (typeof left === \"number\") {\n const parsed = Number(right);\n return Number.isNaN(parsed) ? null : left - parsed;\n }\n\n if (typeof left === \"boolean\") {\n const parsed = right === \"true\" ? true : right === \"false\" ? false : null;\n return parsed === null ? null : Number(left) - Number(parsed);\n }\n\n if (DATE_ONLY.test(right)) {\n const key = dayKey(left);\n return key === null ? null : key.localeCompare(right);\n }\n\n return TEXT_COLLATOR.compare(String(left), right);\n}\n\n/**\n * Equality in the row value's type, falling back to exact text.\n *\n * Case-sensitive on purpose: `eq` maps to a plain `WHERE column = value` on the\n * server side, so a case-insensitive client would quietly disagree with the\n * backend for the same filter. `contains` is the case-insensitive operator, and\n * it is also the default one for text fields, so the friendly behaviour is what\n * people get without asking.\n *\n * Identical strings take a fast path out, since the row value is text in the\n * common case and the collator is the expensive part.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Whether the two are equal.\n */\nfunction equals(left: unknown, right: string): boolean {\n if (left === right) return true;\n const result = compare(left, right);\n if (result !== null) return result === 0;\n return String(left ?? \"\") === right;\n}\n\n/**\n * Is this row value empty?\n *\n * Empty means absent, blank text or an empty list — never falsy. `0` and `false`\n * are values somebody chose, and a filter that hid them would be reporting the\n * wrong count on every dashboard that tracks zeroes.\n *\n * @param value - The row value.\n * @returns Whether the value counts as empty.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * The filter's value as a list, whatever arity the operator uses.\n *\n * @param filter - The filter to read.\n * @returns Its values, trimmed of nothing — a leading space may be meaningful text.\n */\nfunction valuesOf(filter: Filter): string[] {\n if (filter.value === undefined) return [];\n return Array.isArray(filter.value) ? [...filter.value] : [String(filter.value)];\n}\n\n/**\n * Evaluate one operator against one row value.\n *\n * @param operator - The comparison to make.\n * @param left - The row value.\n * @param values - The filter values (one, two for `between`, many for `in`).\n * @returns Whether the row value satisfies the operator.\n */\nfunction matchesOperator(operator: FilterOperator, left: unknown, values: string[]): boolean {\n switch (operator) {\n case \"empty\":\n return isEmptyValue(left);\n case \"notEmpty\":\n return !isEmptyValue(left);\n case \"eq\":\n return equals(left, values[0] ?? \"\");\n case \"ne\":\n return !equals(left, values[0] ?? \"\");\n case \"contains\":\n return String(left ?? \"\")\n .toLowerCase()\n .includes((values[0] ?? \"\").toLowerCase());\n case \"in\":\n return values.some((value) => equals(left, value));\n case \"between\": {\n const [first = \"\", second = \"\"] = values;\n const low = compare(left, first);\n const high = compare(left, second);\n if (low === null || high === null) return false;\n return low >= 0 && high <= 0;\n }\n default: {\n const result = compare(left, values[0] ?? \"\");\n if (result === null) return false;\n if (operator === \"gt\") return result > 0;\n if (operator === \"gte\") return result >= 0;\n if (operator === \"lt\") return result < 0;\n return result <= 0;\n }\n }\n}\n\n/**\n * Order the two values of a `between` so the range is never empty by accident.\n *\n * A person who picks the later date first means the range between the two dates,\n * not \"no rows\". The server-side `BETWEEN` needs `(lo, hi)` in order too, which\n * is why the normalisation lives here and is shared by `filtersToQueryParams`.\n *\n * @param values - The raw pair.\n * @returns The pair, ascending.\n */\nexport function orderRange(values: readonly string[]): string[] {\n const [first = \"\", second = \"\"] = values;\n if (DATE_ONLY.test(first) && DATE_ONLY.test(second)) {\n return first <= second ? [first, second] : [second, first];\n }\n const low = Number(first);\n const high = Number(second);\n if (!Number.isNaN(low) && !Number.isNaN(high)) {\n return low <= high ? [first, second] : [second, first];\n }\n return TEXT_COLLATOR.compare(first, second) <= 0 ? [first, second] : [second, first];\n}\n\n/**\n * Run a filter set over an in-memory list.\n *\n * Closes the loop `FilterBar` opens: the bar produces `Filter[]`, this applies\n * them. Filters combine with `AND`, matching the flat model the bar builds, and\n * an incomplete filter is skipped rather than treated as a match of nothing — a\n * half-filled form should not empty the table underneath it.\n *\n * Comparison follows the row's type, not the filter's: numbers compare\n * numerically, dates compare by day, and everything else compares as text with\n * `numeric: true` so `\"item 2\"` lands before `\"item 10\"`.\n *\n * A few behaviours differ from the SQL the server-side twin produces, and the\n * difference is deliberate rather than accidental:\n *\n * - `ne` matches rows whose value is absent. In SQL, `column <> 'x'` is `NULL`\n * for a `NULL` column and the row drops out. Here \"is not paid\" shows the rows\n * with no status at all, which is what the chip claims.\n * - `empty` matches `NULL`, blank text and empty lists; `__isnull` on the server\n * only matches `NULL`. A column that stores `\"\"` instead of `NULL` is where\n * the two disagree.\n *\n * @example\n * const visible = applyFilters(orders, filters);\n *\n * Each filter's values are normalised once, before the scan, rather than inside\n * the per-row predicate: the arity, the `between` ordering and the value array\n * depend only on the filter, so deriving them per row multiplied that work by\n * the row count. `empty`/`notEmpty` ignore their values entirely, which is why\n * the prepared shape does not need to distinguish them.\n *\n * @param items - The full list.\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns A new array with the rows that satisfy every complete filter.\n */\nexport function applyFilters<T>(items: readonly T[], filters: readonly Filter[]): T[] {\n const active = filters.filter(isComplete).map((filter) => ({\n field: filter.field,\n operator: filter.operator,\n values: filter.operator === \"between\" ? orderRange(valuesOf(filter)) : valuesOf(filter),\n }));\n if (active.length === 0) return [...items];\n\n return items.filter((item) =>\n active.every(({ field, operator, values }) =>\n matchesOperator(operator, readField(item, field), values),\n ),\n );\n}\n"],"mappings":"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"}
|