@m5kdev/frontend 0.8.10 → 0.9.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/packages/backend/dist/src/modules/auth/auth.dto.d.mts +6 -6
- package/dist/packages/backend/dist/src/modules/auth/auth.dto.d.mts.map +1 -1
- package/dist/packages/backend/dist/src/modules/auth/auth.dto.d.ts +6 -6
- package/dist/packages/backend/dist/src/modules/auth/auth.dto.d.ts.map +1 -1
- package/dist/packages/backend/dist/src/modules/billing/billing.repository.d.mts +10 -10
- package/dist/packages/backend/dist/src/modules/billing/billing.repository.d.mts.map +1 -1
- package/dist/packages/backend/dist/src/modules/billing/billing.repository.d.ts +10 -10
- package/dist/packages/backend/dist/src/modules/billing/billing.repository.d.ts.map +1 -1
- package/dist/packages/backend/dist/src/modules/billing/billing.service.d.mts +6 -6
- package/dist/packages/backend/dist/src/modules/billing/billing.service.d.mts.map +1 -1
- package/dist/packages/backend/dist/src/modules/billing/billing.service.d.ts +6 -6
- package/dist/packages/backend/dist/src/modules/billing/billing.service.d.ts.map +1 -1
- package/dist/packages/backend/dist/src/types.d.mts +17 -17
- package/dist/packages/backend/dist/src/types.d.mts.map +1 -1
- package/dist/packages/backend/dist/src/types.d.ts +17 -17
- package/dist/packages/backend/dist/src/types.d.ts.map +1 -1
- package/dist/src/modules/auth/components/AuthProvider.d.mts +1 -1
- package/dist/src/modules/auth/components/AuthProvider.d.mts.map +1 -1
- package/dist/src/modules/auth/components/AuthProvider.d.ts +1 -1
- package/dist/src/modules/auth/components/AuthProvider.d.ts.map +1 -1
- package/dist/src/modules/billing/components/BillingProvider.d.mts +1 -1
- package/dist/src/modules/billing/components/BillingProvider.d.mts.map +1 -1
- package/dist/src/modules/billing/components/BillingProvider.d.ts +1 -1
- package/dist/src/modules/billing/components/BillingProvider.d.ts.map +1 -1
- package/dist/src/modules/table/hooks/useNuqsQueryParams.d.mts +1 -1
- package/dist/src/modules/table/hooks/useNuqsQueryParams.d.mts.map +1 -1
- package/dist/src/modules/table/hooks/useNuqsQueryParams.d.ts +1 -1
- package/dist/src/modules/table/hooks/useNuqsQueryParams.d.ts.map +1 -1
- package/dist/src/modules/table/hooks/useNuqsQueryParams.js +165 -9
- package/dist/src/modules/table/hooks/useNuqsQueryParams.js.map +1 -1
- package/dist/src/modules/table/hooks/useNuqsQueryParams.mjs +167 -11
- package/dist/src/modules/table/hooks/useNuqsQueryParams.mjs.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BillingProvider.d.mts","names":[],"sources":["../../../../../src/modules/billing/components/BillingProvider.tsx"],"mappings":";;;;;;cAKa,sBAAA,EAEQ,OAAA,CAFc,OAAA;;QAE3B,aAAA;AAAA;AAAA,iBAMQ,eAAA,CAAA;EACd,OAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA;AAAA;EAEA,OAAA,EAAS,cAAA;EACT,QAAA,EAAU,KAAA,CAAM,SAAA;EAChB,MAAA,GAAS,KAAA,CAAM,SAAA;EACf,QAAA,EAAU,KAAA,CAAM,SAAA;EAChB,aAAA;AAAA,yCACD,
|
|
1
|
+
{"version":3,"file":"BillingProvider.d.mts","names":[],"sources":["../../../../../src/modules/billing/components/BillingProvider.tsx"],"mappings":";;;;;;cAKa,sBAAA,EAEQ,OAAA,CAFc,OAAA;;QAE3B,aAAA;AAAA;AAAA,iBAMQ,eAAA,CAAA;EACd,OAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA;AAAA;EAEA,OAAA,EAAS,cAAA;EACT,QAAA,EAAU,KAAA,CAAM,SAAA;EAChB,MAAA,GAAS,KAAA,CAAM,SAAA;EACf,QAAA,EAAU,KAAA,CAAM,SAAA;EAChB,aAAA;AAAA,yCACD,QAAA,CAF0B,OAAA,CAE1B,SAAA,IAAA,OAAA,sCAAA,OAAA,CAAA,WAAA,GAAA,OAAA,CAAA,YAAA,mBAAA,OAAA,CAAA,qBAAA,SAAA,QAAA,CAAA,OAAA,CAAA,SAAA,wBAAA,oBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -20,7 +20,7 @@ declare function BillingProvider({
|
|
|
20
20
|
loader?: React.ReactNode;
|
|
21
21
|
planPage: React.ReactNode;
|
|
22
22
|
skipPlanCheck?: boolean;
|
|
23
|
-
}): string | number | bigint | boolean |
|
|
23
|
+
}): string | number | bigint | boolean | Iterable<_$react.ReactNode> | Promise<string | number | bigint | boolean | _$react.ReactPortal | _$react.ReactElement<unknown, string | _$react.JSXElementConstructor<any>> | Iterable<_$react.ReactNode> | null | undefined> | _$react_jsx_runtime0.JSX.Element | null | undefined;
|
|
24
24
|
//#endregion
|
|
25
25
|
export { BillingProvider, billingProviderContext };
|
|
26
26
|
//# sourceMappingURL=BillingProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BillingProvider.d.ts","names":[],"sources":["../../../../../src/modules/billing/components/BillingProvider.tsx"],"mappings":";;;;;;cAKa,sBAAA,EAEQ,OAAA,CAFc,OAAA;;QAE3B,aAAA;AAAA;AAAA,iBAMQ,eAAA,CAAA;EACd,OAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA;AAAA;EAEA,OAAA,EAAS,cAAA;EACT,QAAA,EAAU,KAAA,CAAM,SAAA;EAChB,MAAA,GAAS,KAAA,CAAM,SAAA;EACf,QAAA,EAAU,KAAA,CAAM,SAAA;EAChB,aAAA;AAAA,yCACD,
|
|
1
|
+
{"version":3,"file":"BillingProvider.d.ts","names":[],"sources":["../../../../../src/modules/billing/components/BillingProvider.tsx"],"mappings":";;;;;;cAKa,sBAAA,EAEQ,OAAA,CAFc,OAAA;;QAE3B,aAAA;AAAA;AAAA,iBAMQ,eAAA,CAAA;EACd,OAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA;AAAA;EAEA,OAAA,EAAS,cAAA;EACT,QAAA,EAAU,KAAA,CAAM,SAAA;EAChB,MAAA,GAAS,KAAA,CAAM,SAAA;EACf,QAAA,EAAU,KAAA,CAAM,SAAA;EAChB,aAAA;AAAA,yCACD,QAAA,CAF0B,OAAA,CAE1B,SAAA,IAAA,OAAA,sCAAA,OAAA,CAAA,WAAA,GAAA,OAAA,CAAA,YAAA,mBAAA,OAAA,CAAA,qBAAA,SAAA,QAAA,CAAA,OAAA,CAAA,SAAA,wBAAA,oBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -28,7 +28,7 @@ interface NuqsQueryParams {
|
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Hook to manage all query parameters via nuqs (URL query parameters)
|
|
31
|
-
* Manages: filters
|
|
31
|
+
* Manages: `f` (filters as tuple arrays), `q`, `s`/`o`, `p`/`l`, `g`, `r`
|
|
32
32
|
* Accepts an optional prefix to namespace params when multiple tables share a view.
|
|
33
33
|
*/
|
|
34
34
|
declare const useNuqsQueryParams: (prefix?: string) => NuqsQueryParams;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useNuqsQueryParams.d.mts","names":[],"sources":["../../../../../src/modules/table/hooks/useNuqsQueryParams.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"useNuqsQueryParams.d.mts","names":[],"sources":["../../../../../src/modules/table/hooks/useNuqsQueryParams.ts"],"mappings":";;;;;;;KAoOY,KAAA;AAAA,KAEA,WAAA;AAAA,UAEK,eAAA;;EAEf,CAAA;EACA,IAAA,GAAO,KAAA;EACP,OAAA,GAAU,YAAA;EACV,UAAA,IAAc,OAAA,EAAS,YAAA;EACvB,WAAA,GAAc,WAAA;EACd,cAAA,IAAkB,KAAA,EAAO,WAAA;EACzB,IAAA;EACA,KAAA,GAAQ,KAAA;EACR,UAAA,IAAc,OAAA,EAAS,OAAA,CAAQ,YAAA;EAC/B,OAAA,GAAU,YAAA;EACV,IAAA;EACA,KAAA;EACA,aAAA,IAAiB,OAAA,EAAS,OAAA,CAAQ,eAAA;EAClC,UAAA,GAAa,eAAA;EACb,QAAA,EAAU,aAAA;EACV,WAAA,GAAc,OAAA,EAAS,OAAA,CAAQ,aAAA;AAAA;;;;;;cAQpB,kBAAA,GAAsB,MAAA,cAAkB,eAAA"}
|
|
@@ -28,7 +28,7 @@ interface NuqsQueryParams {
|
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Hook to manage all query parameters via nuqs (URL query parameters)
|
|
31
|
-
* Manages: filters
|
|
31
|
+
* Manages: `f` (filters as tuple arrays), `q`, `s`/`o`, `p`/`l`, `g`, `r`
|
|
32
32
|
* Accepts an optional prefix to namespace params when multiple tables share a view.
|
|
33
33
|
*/
|
|
34
34
|
declare const useNuqsQueryParams: (prefix?: string) => NuqsQueryParams;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useNuqsQueryParams.d.ts","names":[],"sources":["../../../../../src/modules/table/hooks/useNuqsQueryParams.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"useNuqsQueryParams.d.ts","names":[],"sources":["../../../../../src/modules/table/hooks/useNuqsQueryParams.ts"],"mappings":";;;;;;;KAoOY,KAAA;AAAA,KAEA,WAAA;AAAA,UAEK,eAAA;;EAEf,CAAA;EACA,IAAA,GAAO,KAAA;EACP,OAAA,GAAU,YAAA;EACV,UAAA,IAAc,OAAA,EAAS,YAAA;EACvB,WAAA,GAAc,WAAA;EACd,cAAA,IAAkB,KAAA,EAAO,WAAA;EACzB,IAAA;EACA,KAAA,GAAQ,KAAA;EACR,UAAA,IAAc,OAAA,EAAS,OAAA,CAAQ,YAAA;EAC/B,OAAA,GAAU,YAAA;EACV,IAAA;EACA,KAAA;EACA,aAAA,IAAiB,OAAA,EAAS,OAAA,CAAQ,eAAA;EAClC,UAAA,GAAa,eAAA;EACb,QAAA,EAAU,aAAA;EACV,WAAA,GAAc,OAAA,EAAS,OAAA,CAAQ,aAAA;AAAA;;;;;;cAQpB,kBAAA,GAAsB,MAAA,cAAkB,eAAA"}
|
|
@@ -2,32 +2,188 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let react = require("react");
|
|
3
3
|
let _m5kdev_commons_modules_schemas_query_schema = require("@m5kdev/commons/modules/schemas/query.schema");
|
|
4
4
|
let nuqs = require("nuqs");
|
|
5
|
+
let zod = require("zod");
|
|
5
6
|
//#region src/modules/table/hooks/useNuqsQueryParams.ts
|
|
6
|
-
|
|
7
|
+
/** Each row: [columnId, type, method, value, valueTo?, endColumnId?] — validated after parse */
|
|
8
|
+
const filtersRawSchema = zod.z.array(zod.z.array(zod.z.string()).min(4).max(6));
|
|
9
|
+
/** Escape `\`, `|`, `;` so we can split filters without JSON quotes/brackets. */
|
|
10
|
+
function escapeFilterField(raw) {
|
|
11
|
+
return raw.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/;/g, "\\;");
|
|
12
|
+
}
|
|
13
|
+
function unescapeFilterField(encoded) {
|
|
14
|
+
let out = "";
|
|
15
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
16
|
+
const c = encoded[i];
|
|
17
|
+
if (c === "\\" && i + 1 < encoded.length) {
|
|
18
|
+
const next = encoded[i + 1];
|
|
19
|
+
if (next === "\\" || next === "|" || next === ";") {
|
|
20
|
+
out += next;
|
|
21
|
+
i++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
out += c;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Split on `delimiter` only when not escaped as `\` + delimiter.
|
|
31
|
+
*/
|
|
32
|
+
function splitByEscapedDelimiter(s, delimiter) {
|
|
33
|
+
if (delimiter.length !== 1) throw new Error("splitByEscapedDelimiter expects a single-character delimiter");
|
|
34
|
+
const parts = [];
|
|
35
|
+
let buf = "";
|
|
36
|
+
for (let i = 0; i < s.length; i++) {
|
|
37
|
+
const c = s[i];
|
|
38
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
39
|
+
buf += c + s[i + 1];
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (c === delimiter) {
|
|
44
|
+
parts.push(unescapeFilterField(buf));
|
|
45
|
+
buf = "";
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
buf += c;
|
|
49
|
+
}
|
|
50
|
+
parts.push(unescapeFilterField(buf));
|
|
51
|
+
return parts;
|
|
52
|
+
}
|
|
53
|
+
function serializeFiltersParam(rows) {
|
|
54
|
+
if (rows.length === 0) return "";
|
|
55
|
+
return rows.map((row) => row.map(escapeFilterField).join("|")).join(";");
|
|
56
|
+
}
|
|
57
|
+
function parseFiltersParam(value) {
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
if (trimmed === "") return [];
|
|
60
|
+
const rowStrings = splitByEscapedDelimiter(trimmed, ";");
|
|
61
|
+
const rows = [];
|
|
62
|
+
for (const row of rowStrings) {
|
|
63
|
+
if (row === "") continue;
|
|
64
|
+
const fields = splitByEscapedDelimiter(row, "|");
|
|
65
|
+
if (fields.length >= 4 && fields.length <= 6) rows.push(fields);
|
|
66
|
+
}
|
|
67
|
+
const parsed = filtersRawSchema.safeParse(rows);
|
|
68
|
+
return parsed.success ? parsed.data : null;
|
|
69
|
+
}
|
|
70
|
+
const parseAsFilterTuples = (0, nuqs.createParser)({
|
|
71
|
+
parse: (value) => parseFiltersParam(value),
|
|
72
|
+
serialize: (rows) => serializeFiltersParam(rows),
|
|
73
|
+
eq: (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
74
|
+
}).withDefault([]);
|
|
7
75
|
const parseAsGrouping = (0, nuqs.parseAsArrayOf)(nuqs.parseAsString).withDefault([]);
|
|
76
|
+
const NULLISH_METHODS = new Set([
|
|
77
|
+
"isEmpty",
|
|
78
|
+
"isNotEmpty",
|
|
79
|
+
"is_null",
|
|
80
|
+
"is_not_null"
|
|
81
|
+
]);
|
|
82
|
+
function encodeFilterValue(filter) {
|
|
83
|
+
if (NULLISH_METHODS.has(filter.method)) return typeof filter.value === "string" ? filter.value : "";
|
|
84
|
+
const { type, method, value } = filter;
|
|
85
|
+
switch (type) {
|
|
86
|
+
case "number": return String(value);
|
|
87
|
+
case "boolean": return value ? "true" : "false";
|
|
88
|
+
case "enum":
|
|
89
|
+
case "jsonArray":
|
|
90
|
+
if (method === "oneOf" && Array.isArray(value)) return JSON.stringify(value);
|
|
91
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
92
|
+
return String(value);
|
|
93
|
+
default: return String(value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function decodeFilterValue(type, method, valueStr) {
|
|
97
|
+
if (NULLISH_METHODS.has(method)) return valueStr;
|
|
98
|
+
switch (type) {
|
|
99
|
+
case "number": {
|
|
100
|
+
const n = Number.parseFloat(valueStr);
|
|
101
|
+
return Number.isNaN(n) ? 0 : n;
|
|
102
|
+
}
|
|
103
|
+
case "boolean": return valueStr === "true";
|
|
104
|
+
case "enum":
|
|
105
|
+
case "jsonArray":
|
|
106
|
+
if (method === "oneOf") try {
|
|
107
|
+
const parsed = JSON.parse(valueStr);
|
|
108
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
if (valueStr.startsWith("[")) try {
|
|
113
|
+
const parsed = JSON.parse(valueStr);
|
|
114
|
+
if (Array.isArray(parsed)) return parsed.map(String);
|
|
115
|
+
} catch {}
|
|
116
|
+
return valueStr;
|
|
117
|
+
default: return valueStr;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Builds URL tuple; omits optional tails when unused (endColumnId requires valueTo slot — may be ""). */
|
|
121
|
+
function encodeFilterToTuple(filter) {
|
|
122
|
+
const tuple = [
|
|
123
|
+
filter.columnId,
|
|
124
|
+
filter.type,
|
|
125
|
+
filter.method,
|
|
126
|
+
encodeFilterValue(filter)
|
|
127
|
+
];
|
|
128
|
+
const hasValueTo = filter.valueTo != null && filter.valueTo !== "";
|
|
129
|
+
const hasEndColumnId = filter.endColumnId != null && filter.endColumnId !== "";
|
|
130
|
+
if (hasValueTo && filter.valueTo != null) tuple.push(filter.valueTo);
|
|
131
|
+
if (hasEndColumnId && filter.endColumnId != null) {
|
|
132
|
+
if (!hasValueTo) tuple.push("");
|
|
133
|
+
tuple.push(filter.endColumnId);
|
|
134
|
+
}
|
|
135
|
+
return tuple;
|
|
136
|
+
}
|
|
137
|
+
function decodeTupleToFilter(parts) {
|
|
138
|
+
if (parts.length < 4 || parts.length > 6) return null;
|
|
139
|
+
const [columnId, typeStr, methodStr, valueStr, valueToStr, endColumnIdStr] = parts;
|
|
140
|
+
const typeParsed = _m5kdev_commons_modules_schemas_query_schema.filterSchema.shape.type.safeParse(typeStr);
|
|
141
|
+
const methodParsed = _m5kdev_commons_modules_schemas_query_schema.filterSchema.shape.method.safeParse(methodStr);
|
|
142
|
+
if (!typeParsed.success || !methodParsed.success) return null;
|
|
143
|
+
const type = typeParsed.data;
|
|
144
|
+
const method = methodParsed.data;
|
|
145
|
+
const candidate = {
|
|
146
|
+
columnId,
|
|
147
|
+
type,
|
|
148
|
+
method,
|
|
149
|
+
value: decodeFilterValue(type, method, valueStr)
|
|
150
|
+
};
|
|
151
|
+
if (valueToStr !== void 0) candidate.valueTo = valueToStr;
|
|
152
|
+
if (endColumnIdStr !== void 0) candidate.endColumnId = endColumnIdStr;
|
|
153
|
+
const parsed = _m5kdev_commons_modules_schemas_query_schema.filterSchema.safeParse(candidate);
|
|
154
|
+
return parsed.success ? parsed.data : null;
|
|
155
|
+
}
|
|
8
156
|
/**
|
|
9
157
|
* Hook to manage all query parameters via nuqs (URL query parameters)
|
|
10
|
-
* Manages: filters
|
|
158
|
+
* Manages: `f` (filters as tuple arrays), `q`, `s`/`o`, `p`/`l`, `g`, `r`
|
|
11
159
|
* Accepts an optional prefix to namespace params when multiple tables share a view.
|
|
12
160
|
*/
|
|
13
161
|
const useNuqsQueryParams = (prefix) => {
|
|
14
162
|
const k = (name) => prefix ? `${prefix}_${name}` : name;
|
|
15
|
-
const [sort, setSort] = (0, nuqs.useQueryState)(k("
|
|
16
|
-
const [order, setOrder] = (0, nuqs.useQueryState)(k("
|
|
17
|
-
const [page, setPage] = (0, nuqs.useQueryState)(k("
|
|
18
|
-
const [limit, setLimit] = (0, nuqs.useQueryState)(k("
|
|
163
|
+
const [sort, setSort] = (0, nuqs.useQueryState)(k("s"), nuqs.parseAsString.withDefault(""));
|
|
164
|
+
const [order, setOrder] = (0, nuqs.useQueryState)(k("o"), (0, nuqs.parseAsStringLiteral)(["asc", "desc"]));
|
|
165
|
+
const [page, setPage] = (0, nuqs.useQueryState)(k("p"), nuqs.parseAsInteger.withDefault(1));
|
|
166
|
+
const [limit, setLimit] = (0, nuqs.useQueryState)(k("l"), nuqs.parseAsInteger.withDefault(10));
|
|
19
167
|
const [qRaw, setQRaw] = (0, nuqs.useQueryState)(k("q"), nuqs.parseAsString);
|
|
20
|
-
const [
|
|
168
|
+
const [filtersRaw, setFiltersRaw] = (0, nuqs.useQueryState)(k("f"), parseAsFilterTuples);
|
|
169
|
+
const filters = (0, react.useMemo)(() => {
|
|
170
|
+
const decoded = filtersRaw.map((row) => decodeTupleToFilter(row)).filter((f) => f != null);
|
|
171
|
+
const parsed = _m5kdev_commons_modules_schemas_query_schema.filtersSchema.safeParse(decoded);
|
|
172
|
+
return parsed.success ? parsed.data : [];
|
|
173
|
+
}, [filtersRaw]);
|
|
174
|
+
const setFilters = (0, react.useCallback)((next) => {
|
|
175
|
+
setFiltersRaw(next.length === 0 ? null : next.map(encodeFilterToTuple));
|
|
176
|
+
}, [setFiltersRaw]);
|
|
21
177
|
const setQ = (0, react.useCallback)((value) => {
|
|
22
178
|
setQRaw(value === "" || value == null ? null : value);
|
|
23
179
|
}, [setQRaw]);
|
|
24
|
-
const [granularity, setGranularity] = (0, nuqs.useQueryState)(k("
|
|
180
|
+
const [granularity, setGranularity] = (0, nuqs.useQueryState)(k("r"), (0, nuqs.parseAsStringLiteral)([
|
|
25
181
|
"daily",
|
|
26
182
|
"weekly",
|
|
27
183
|
"monthly",
|
|
28
184
|
"yearly"
|
|
29
185
|
]).withDefault("daily"));
|
|
30
|
-
const [groupingRaw, setGroupingRaw] = (0, nuqs.useQueryState)(k("
|
|
186
|
+
const [groupingRaw, setGroupingRaw] = (0, nuqs.useQueryState)(k("g"), parseAsGrouping);
|
|
31
187
|
const grouping = groupingRaw;
|
|
32
188
|
const setGrouping = (0, react.useCallback)((updater) => {
|
|
33
189
|
const next = typeof updater === "function" ? updater(groupingRaw) : updater;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useNuqsQueryParams.js","names":["filtersSchema","parseAsString","parseAsInteger"],"sources":["../../../../../src/modules/table/hooks/useNuqsQueryParams.ts"],"sourcesContent":["import type { QueryFilters } from \"@m5kdev/commons/modules/schemas/query.schema\";\r\nimport { filtersSchema } from \"@m5kdev/commons/modules/schemas/query.schema\";\r\nimport type { GroupingState, PaginationState, SortingState, Updater } from \"@tanstack/react-table\";\r\nimport {\r\n parseAsArrayOf,\r\n parseAsInteger,\r\n parseAsJson,\r\n parseAsString,\r\n parseAsStringLiteral,\r\n useQueryState,\r\n} from \"nuqs\";\r\nimport { useCallback, useEffect, useMemo } from \"react\";\r\n\r\nconst parseAsFilters = parseAsJson<QueryFilters>((value) => filtersSchema.parse(value)).withDefault(\r\n []\r\n);\r\n\r\nconst parseAsGrouping = parseAsArrayOf(parseAsString).withDefault([]);\r\n\r\nexport type Order = \"asc\" | \"desc\";\r\n\r\nexport type Granularity = \"daily\" | \"weekly\" | \"monthly\" | \"yearly\";\r\n\r\nexport interface NuqsQueryParams {\r\n /** Global search string from URL (`q`); absent when not in URL. */\r\n q: string | null;\r\n setQ: (value: string | null) => void;\r\n filters?: QueryFilters;\r\n setFilters?: (filters: QueryFilters) => void;\r\n granularity?: Granularity;\r\n setGranularity?: (value: Granularity) => void;\r\n sort?: string;\r\n order?: Order | null;\r\n setSorting?: (updater: Updater<SortingState>) => void;\r\n sorting?: SortingState;\r\n page?: number;\r\n limit?: number;\r\n setPagination?: (updater: Updater<PaginationState>) => void;\r\n pagination?: PaginationState;\r\n grouping: GroupingState;\r\n setGrouping: (updater: Updater<GroupingState>) => void;\r\n}\r\n\r\n/**\r\n * Hook to manage all query parameters via nuqs (URL query parameters)\r\n * Manages: filters, q, sort, order, page, limit, groupBy, granularity\r\n * Accepts an optional prefix to namespace params when multiple tables share a view.\r\n */\r\nexport const useNuqsQueryParams = (prefix?: string): NuqsQueryParams => {\r\n const k = (name: string) => (prefix ? `${prefix}_${name}` : name);\r\n\r\n const [sort, setSort] = useQueryState<string>(k(\"sort\"), parseAsString.withDefault(\"\"));\r\n const [order, setOrder] = useQueryState<Order>(k(\"order\"), parseAsStringLiteral([\"asc\", \"desc\"]));\r\n const [page, setPage] = useQueryState<number>(k(\"page\"), parseAsInteger.withDefault(1));\r\n const [limit, setLimit] = useQueryState<number>(k(\"limit\"), parseAsInteger.withDefault(10));\r\n const [qRaw, setQRaw] = useQueryState(k(\"q\"), parseAsString);\r\n const [filters, setFilters] = useQueryState<QueryFilters>(k(\"filters\"), parseAsFilters);\r\n\r\n const setQ = useCallback(\r\n (value: string | null) => {\r\n void setQRaw(value === \"\" || value == null ? null : value);\r\n },\r\n [setQRaw]\r\n );\r\n const [granularity, setGranularity] = useQueryState<Granularity>(\r\n k(\"granularity\"),\r\n parseAsStringLiteral([\"daily\", \"weekly\", \"monthly\", \"yearly\"]).withDefault(\"daily\")\r\n );\r\n const [groupingRaw, setGroupingRaw] = useQueryState<string[]>(k(\"groupBy\"), parseAsGrouping);\r\n const grouping: GroupingState = groupingRaw;\r\n\r\n const setGrouping = useCallback(\r\n (updater: Updater<GroupingState>) => {\r\n const next = typeof updater === \"function\" ? updater(groupingRaw) : updater;\r\n setGroupingRaw(next.length > 0 ? next : null);\r\n },\r\n [groupingRaw, setGroupingRaw]\r\n );\r\n\r\n const sorting = useMemo(() => {\r\n if (!sort) {\r\n return [];\r\n }\r\n const effectiveOrder = order ?? \"asc\";\r\n return [{ id: sort, desc: effectiveOrder === \"desc\" }];\r\n }, [sort, order]);\r\n\r\n const pagination = useMemo(() => ({ pageIndex: page - 1, pageSize: limit }), [page, limit]);\r\n\r\n const setSorting = useCallback(\r\n (updater: Updater<SortingState>) => {\r\n // Build current sorting state from current values\r\n const currentSorting: SortingState = sort\r\n ? [{ id: sort, desc: (order ?? \"asc\") === \"desc\" }]\r\n : [];\r\n\r\n const next = typeof updater === \"function\" ? updater(currentSorting) : updater;\r\n const first = next[0];\r\n\r\n if (!first || !first.id) {\r\n setSort(\"\");\r\n setOrder(null);\r\n return;\r\n }\r\n\r\n setSort(first.id);\r\n setOrder(first.desc ? \"desc\" : \"asc\");\r\n },\r\n [sort, order, setSort, setOrder]\r\n );\r\n\r\n // Sync order to \"asc\" if sort is set but order is null (handles URL state inconsistencies)\r\n useEffect(() => {\r\n if (sort && !order) {\r\n setOrder(\"asc\");\r\n }\r\n }, [sort, order, setOrder]);\r\n\r\n // biome-ignore lint/correctness/useExhaustiveDependencies: dependent on page and limit only\r\n const setPagination = useCallback(\r\n (updater: Updater<PaginationState>) => {\r\n // TanStack Table uses 0-based indexing, but URL uses 1-based\r\n const currentState = { pageIndex: page - 1, pageSize: limit };\r\n const next = typeof updater === \"function\" ? updater(currentState) : updater;\r\n // Convert back to 1-based for URL\r\n setPage((next.pageIndex ?? 0) + 1);\r\n setLimit(next.pageSize ?? 10);\r\n },\r\n [page, limit]\r\n );\r\n\r\n return useMemo(\r\n () => ({\r\n q: qRaw,\r\n setQ,\r\n filters,\r\n setFilters,\r\n granularity,\r\n setGranularity,\r\n sort,\r\n order,\r\n setSorting,\r\n sorting,\r\n page,\r\n limit,\r\n setPagination,\r\n pagination,\r\n grouping,\r\n setGrouping,\r\n }),\r\n [\r\n qRaw,\r\n setQ,\r\n filters,\r\n setFilters,\r\n granularity,\r\n setGranularity,\r\n sort,\r\n order,\r\n setSorting,\r\n sorting,\r\n page,\r\n limit,\r\n setPagination,\r\n pagination,\r\n grouping,\r\n setGrouping,\r\n ]\r\n );\r\n};\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useNuqsQueryParams.js","names":["z","parseAsString","filterSchema","parseAsInteger","filtersSchema"],"sources":["../../../../../src/modules/table/hooks/useNuqsQueryParams.ts"],"sourcesContent":["import type { QueryFilter, QueryFilters } from \"@m5kdev/commons/modules/schemas/query.schema\";\r\nimport { filterSchema, filtersSchema } from \"@m5kdev/commons/modules/schemas/query.schema\";\r\nimport type { GroupingState, PaginationState, SortingState, Updater } from \"@tanstack/react-table\";\r\nimport {\r\n createParser,\r\n parseAsArrayOf,\r\n parseAsInteger,\r\n parseAsString,\r\n parseAsStringLiteral,\r\n useQueryState,\r\n} from \"nuqs\";\r\nimport { useCallback, useEffect, useMemo } from \"react\";\r\nimport { z } from \"zod\";\r\n\r\n/** Each row: [columnId, type, method, value, valueTo?, endColumnId?] — validated after parse */\r\nconst filtersRawSchema = z.array(z.array(z.string()).min(4).max(6));\r\n\r\n/** Escape `\\`, `|`, `;` so we can split filters without JSON quotes/brackets. */\r\nfunction escapeFilterField(raw: string): string {\r\n return raw.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\|/g, \"\\\\|\").replace(/;/g, \"\\\\;\");\r\n}\r\n\r\nfunction unescapeFilterField(encoded: string): string {\r\n let out = \"\";\r\n for (let i = 0; i < encoded.length; i++) {\r\n const c = encoded[i];\r\n if (c === \"\\\\\" && i + 1 < encoded.length) {\r\n const next = encoded[i + 1];\r\n if (next === \"\\\\\" || next === \"|\" || next === \";\") {\r\n out += next;\r\n i++;\r\n continue;\r\n }\r\n }\r\n out += c;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Split on `delimiter` only when not escaped as `\\` + delimiter.\r\n */\r\nfunction splitByEscapedDelimiter(s: string, delimiter: string): string[] {\r\n if (delimiter.length !== 1) {\r\n throw new Error(\"splitByEscapedDelimiter expects a single-character delimiter\");\r\n }\r\n const parts: string[] = [];\r\n let buf = \"\";\r\n for (let i = 0; i < s.length; i++) {\r\n const c = s[i];\r\n if (c === \"\\\\\" && i + 1 < s.length) {\r\n buf += c + s[i + 1];\r\n i++;\r\n continue;\r\n }\r\n if (c === delimiter) {\r\n parts.push(unescapeFilterField(buf));\r\n buf = \"\";\r\n continue;\r\n }\r\n buf += c;\r\n }\r\n parts.push(unescapeFilterField(buf));\r\n return parts;\r\n}\r\n\r\nfunction serializeFiltersParam(rows: string[][]): string {\r\n if (rows.length === 0) {\r\n return \"\";\r\n }\r\n return rows.map((row) => row.map(escapeFilterField).join(\"|\")).join(\";\");\r\n}\r\n\r\nfunction parseFiltersParam(value: string): string[][] | null {\r\n const trimmed = value.trim();\r\n if (trimmed === \"\") {\r\n return [];\r\n }\r\n const rowStrings = splitByEscapedDelimiter(trimmed, \";\");\r\n const rows: string[][] = [];\r\n for (const row of rowStrings) {\r\n if (row === \"\") {\r\n continue;\r\n }\r\n const fields = splitByEscapedDelimiter(row, \"|\");\r\n if (fields.length >= 4 && fields.length <= 6) {\r\n rows.push(fields);\r\n }\r\n }\r\n const parsed = filtersRawSchema.safeParse(rows);\r\n return parsed.success ? parsed.data : null;\r\n}\r\n\r\nconst parseAsFilterTuples = createParser<string[][]>({\r\n parse: (value) => parseFiltersParam(value),\r\n serialize: (rows) => serializeFiltersParam(rows),\r\n eq: (a, b) => JSON.stringify(a) === JSON.stringify(b),\r\n}).withDefault([]);\r\n\r\nconst parseAsGrouping = parseAsArrayOf(parseAsString).withDefault([]);\r\n\r\nconst NULLISH_METHODS: ReadonlySet<QueryFilter[\"method\"]> = new Set([\r\n \"isEmpty\",\r\n \"isNotEmpty\",\r\n \"is_null\",\r\n \"is_not_null\",\r\n]);\r\n\r\nfunction encodeFilterValue(filter: QueryFilter): string {\r\n if (NULLISH_METHODS.has(filter.method)) {\r\n return typeof filter.value === \"string\" ? filter.value : \"\";\r\n }\r\n const { type, method, value } = filter;\r\n switch (type) {\r\n case \"number\":\r\n return String(value as number);\r\n case \"boolean\":\r\n return (value as boolean) ? \"true\" : \"false\";\r\n case \"enum\":\r\n case \"jsonArray\":\r\n if (method === \"oneOf\" && Array.isArray(value)) {\r\n return JSON.stringify(value);\r\n }\r\n if (Array.isArray(value)) {\r\n return JSON.stringify(value);\r\n }\r\n return String(value);\r\n default:\r\n return String(value);\r\n }\r\n}\r\n\r\nfunction decodeFilterValue(\r\n type: QueryFilter[\"type\"],\r\n method: QueryFilter[\"method\"],\r\n valueStr: string\r\n): QueryFilter[\"value\"] {\r\n if (NULLISH_METHODS.has(method)) {\r\n return valueStr;\r\n }\r\n switch (type) {\r\n case \"number\": {\r\n const n = Number.parseFloat(valueStr);\r\n return Number.isNaN(n) ? 0 : n;\r\n }\r\n case \"boolean\":\r\n return valueStr === \"true\";\r\n case \"enum\":\r\n case \"jsonArray\":\r\n if (method === \"oneOf\") {\r\n try {\r\n const parsed = JSON.parse(valueStr) as unknown;\r\n return Array.isArray(parsed) ? parsed.map(String) : [];\r\n } catch {\r\n return [];\r\n }\r\n }\r\n if (valueStr.startsWith(\"[\")) {\r\n try {\r\n const parsed = JSON.parse(valueStr) as unknown;\r\n if (Array.isArray(parsed)) {\r\n return parsed.map(String);\r\n }\r\n } catch {\r\n /* use raw string */\r\n }\r\n }\r\n return valueStr;\r\n default:\r\n return valueStr;\r\n }\r\n}\r\n\r\n/** Builds URL tuple; omits optional tails when unused (endColumnId requires valueTo slot — may be \"\"). */\r\nfunction encodeFilterToTuple(filter: QueryFilter): string[] {\r\n const tuple: string[] = [filter.columnId, filter.type, filter.method, encodeFilterValue(filter)];\r\n const hasValueTo = filter.valueTo != null && filter.valueTo !== \"\";\r\n const hasEndColumnId = filter.endColumnId != null && filter.endColumnId !== \"\";\r\n\r\n if (hasValueTo && filter.valueTo != null) {\r\n tuple.push(filter.valueTo);\r\n }\r\n\r\n if (hasEndColumnId && filter.endColumnId != null) {\r\n if (!hasValueTo) {\r\n tuple.push(\"\");\r\n }\r\n tuple.push(filter.endColumnId);\r\n }\r\n\r\n return tuple;\r\n}\r\n\r\nfunction decodeTupleToFilter(parts: readonly string[]): QueryFilter | null {\r\n if (parts.length < 4 || parts.length > 6) {\r\n return null;\r\n }\r\n\r\n const [columnId, typeStr, methodStr, valueStr, valueToStr, endColumnIdStr] = parts;\r\n\r\n const typeParsed = filterSchema.shape.type.safeParse(typeStr);\r\n const methodParsed = filterSchema.shape.method.safeParse(methodStr);\r\n if (!typeParsed.success || !methodParsed.success) {\r\n return null;\r\n }\r\n\r\n const type = typeParsed.data;\r\n const method = methodParsed.data;\r\n const value = decodeFilterValue(type, method, valueStr);\r\n\r\n const candidate: QueryFilter = {\r\n columnId,\r\n type,\r\n method,\r\n value,\r\n };\r\n\r\n if (valueToStr !== undefined) {\r\n candidate.valueTo = valueToStr;\r\n }\r\n if (endColumnIdStr !== undefined) {\r\n candidate.endColumnId = endColumnIdStr;\r\n }\r\n\r\n const parsed = filterSchema.safeParse(candidate);\r\n return parsed.success ? parsed.data : null;\r\n}\r\n\r\nexport type Order = \"asc\" | \"desc\";\r\n\r\nexport type Granularity = \"daily\" | \"weekly\" | \"monthly\" | \"yearly\";\r\n\r\nexport interface NuqsQueryParams {\r\n /** Global search string from URL (`q`); absent when not in URL. */\r\n q: string | null;\r\n setQ: (value: string | null) => void;\r\n filters?: QueryFilters;\r\n setFilters?: (filters: QueryFilters) => void;\r\n granularity?: Granularity;\r\n setGranularity?: (value: Granularity) => void;\r\n sort?: string;\r\n order?: Order | null;\r\n setSorting?: (updater: Updater<SortingState>) => void;\r\n sorting?: SortingState;\r\n page?: number;\r\n limit?: number;\r\n setPagination?: (updater: Updater<PaginationState>) => void;\r\n pagination?: PaginationState;\r\n grouping: GroupingState;\r\n setGrouping: (updater: Updater<GroupingState>) => void;\r\n}\r\n\r\n/**\r\n * Hook to manage all query parameters via nuqs (URL query parameters)\r\n * Manages: `f` (filters as tuple arrays), `q`, `s`/`o`, `p`/`l`, `g`, `r`\r\n * Accepts an optional prefix to namespace params when multiple tables share a view.\r\n */\r\nexport const useNuqsQueryParams = (prefix?: string): NuqsQueryParams => {\r\n const k = (name: string) => (prefix ? `${prefix}_${name}` : name);\r\n\r\n const [sort, setSort] = useQueryState<string>(k(\"s\"), parseAsString.withDefault(\"\"));\r\n const [order, setOrder] = useQueryState<Order>(k(\"o\"), parseAsStringLiteral([\"asc\", \"desc\"]));\r\n const [page, setPage] = useQueryState<number>(k(\"p\"), parseAsInteger.withDefault(1));\r\n const [limit, setLimit] = useQueryState<number>(k(\"l\"), parseAsInteger.withDefault(10));\r\n const [qRaw, setQRaw] = useQueryState(k(\"q\"), parseAsString);\r\n const [filtersRaw, setFiltersRaw] = useQueryState<string[][]>(k(\"f\"), parseAsFilterTuples);\r\n\r\n const filters = useMemo((): QueryFilters => {\r\n const decoded = filtersRaw\r\n .map((row) => decodeTupleToFilter(row))\r\n .filter((f): f is QueryFilter => f != null);\r\n const parsed = filtersSchema.safeParse(decoded);\r\n return parsed.success ? parsed.data : [];\r\n }, [filtersRaw]);\r\n\r\n const setFilters = useCallback(\r\n (next: QueryFilters) => {\r\n void setFiltersRaw(next.length === 0 ? null : next.map(encodeFilterToTuple));\r\n },\r\n [setFiltersRaw]\r\n );\r\n\r\n const setQ = useCallback(\r\n (value: string | null) => {\r\n void setQRaw(value === \"\" || value == null ? null : value);\r\n },\r\n [setQRaw]\r\n );\r\n const [granularity, setGranularity] = useQueryState<Granularity>(\r\n k(\"r\"),\r\n parseAsStringLiteral([\"daily\", \"weekly\", \"monthly\", \"yearly\"]).withDefault(\"daily\")\r\n );\r\n const [groupingRaw, setGroupingRaw] = useQueryState<string[]>(k(\"g\"), parseAsGrouping);\r\n const grouping: GroupingState = groupingRaw;\r\n\r\n const setGrouping = useCallback(\r\n (updater: Updater<GroupingState>) => {\r\n const next = typeof updater === \"function\" ? updater(groupingRaw) : updater;\r\n setGroupingRaw(next.length > 0 ? next : null);\r\n },\r\n [groupingRaw, setGroupingRaw]\r\n );\r\n\r\n const sorting = useMemo(() => {\r\n if (!sort) {\r\n return [];\r\n }\r\n const effectiveOrder = order ?? \"asc\";\r\n return [{ id: sort, desc: effectiveOrder === \"desc\" }];\r\n }, [sort, order]);\r\n\r\n const pagination = useMemo(() => ({ pageIndex: page - 1, pageSize: limit }), [page, limit]);\r\n\r\n const setSorting = useCallback(\r\n (updater: Updater<SortingState>) => {\r\n const currentSorting: SortingState = sort\r\n ? [{ id: sort, desc: (order ?? \"asc\") === \"desc\" }]\r\n : [];\r\n\r\n const next = typeof updater === \"function\" ? updater(currentSorting) : updater;\r\n const first = next[0];\r\n\r\n if (!first || !first.id) {\r\n setSort(\"\");\r\n setOrder(null);\r\n return;\r\n }\r\n\r\n setSort(first.id);\r\n setOrder(first.desc ? \"desc\" : \"asc\");\r\n },\r\n [sort, order, setSort, setOrder]\r\n );\r\n\r\n useEffect(() => {\r\n if (sort && !order) {\r\n setOrder(\"asc\");\r\n }\r\n }, [sort, order, setOrder]);\r\n\r\n // biome-ignore lint/correctness/useExhaustiveDependencies: dependent on page and limit only\r\n const setPagination = useCallback(\r\n (updater: Updater<PaginationState>) => {\r\n const currentState = { pageIndex: page - 1, pageSize: limit };\r\n const next = typeof updater === \"function\" ? updater(currentState) : updater;\r\n setPage((next.pageIndex ?? 0) + 1);\r\n setLimit(next.pageSize ?? 10);\r\n },\r\n [page, limit]\r\n );\r\n\r\n return useMemo(\r\n () => ({\r\n q: qRaw,\r\n setQ,\r\n filters,\r\n setFilters,\r\n granularity,\r\n setGranularity,\r\n sort,\r\n order,\r\n setSorting,\r\n sorting,\r\n page,\r\n limit,\r\n setPagination,\r\n pagination,\r\n grouping,\r\n setGrouping,\r\n }),\r\n [\r\n qRaw,\r\n setQ,\r\n filters,\r\n setFilters,\r\n granularity,\r\n setGranularity,\r\n sort,\r\n order,\r\n setSorting,\r\n sorting,\r\n page,\r\n limit,\r\n setPagination,\r\n pagination,\r\n grouping,\r\n setGrouping,\r\n ]\r\n );\r\n};\r\n"],"mappings":"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"}
|
|
@@ -1,32 +1,188 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo } from "react";
|
|
2
|
-
import { filtersSchema } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
3
|
-
import { parseAsArrayOf, parseAsInteger,
|
|
2
|
+
import { filterSchema, filtersSchema } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
3
|
+
import { createParser, parseAsArrayOf, parseAsInteger, parseAsString, parseAsStringLiteral, useQueryState } from "nuqs";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
//#region src/modules/table/hooks/useNuqsQueryParams.ts
|
|
5
|
-
|
|
6
|
+
/** Each row: [columnId, type, method, value, valueTo?, endColumnId?] — validated after parse */
|
|
7
|
+
const filtersRawSchema = z.array(z.array(z.string()).min(4).max(6));
|
|
8
|
+
/** Escape `\`, `|`, `;` so we can split filters without JSON quotes/brackets. */
|
|
9
|
+
function escapeFilterField(raw) {
|
|
10
|
+
return raw.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/;/g, "\\;");
|
|
11
|
+
}
|
|
12
|
+
function unescapeFilterField(encoded) {
|
|
13
|
+
let out = "";
|
|
14
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
15
|
+
const c = encoded[i];
|
|
16
|
+
if (c === "\\" && i + 1 < encoded.length) {
|
|
17
|
+
const next = encoded[i + 1];
|
|
18
|
+
if (next === "\\" || next === "|" || next === ";") {
|
|
19
|
+
out += next;
|
|
20
|
+
i++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
out += c;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Split on `delimiter` only when not escaped as `\` + delimiter.
|
|
30
|
+
*/
|
|
31
|
+
function splitByEscapedDelimiter(s, delimiter) {
|
|
32
|
+
if (delimiter.length !== 1) throw new Error("splitByEscapedDelimiter expects a single-character delimiter");
|
|
33
|
+
const parts = [];
|
|
34
|
+
let buf = "";
|
|
35
|
+
for (let i = 0; i < s.length; i++) {
|
|
36
|
+
const c = s[i];
|
|
37
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
38
|
+
buf += c + s[i + 1];
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (c === delimiter) {
|
|
43
|
+
parts.push(unescapeFilterField(buf));
|
|
44
|
+
buf = "";
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
buf += c;
|
|
48
|
+
}
|
|
49
|
+
parts.push(unescapeFilterField(buf));
|
|
50
|
+
return parts;
|
|
51
|
+
}
|
|
52
|
+
function serializeFiltersParam(rows) {
|
|
53
|
+
if (rows.length === 0) return "";
|
|
54
|
+
return rows.map((row) => row.map(escapeFilterField).join("|")).join(";");
|
|
55
|
+
}
|
|
56
|
+
function parseFiltersParam(value) {
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
if (trimmed === "") return [];
|
|
59
|
+
const rowStrings = splitByEscapedDelimiter(trimmed, ";");
|
|
60
|
+
const rows = [];
|
|
61
|
+
for (const row of rowStrings) {
|
|
62
|
+
if (row === "") continue;
|
|
63
|
+
const fields = splitByEscapedDelimiter(row, "|");
|
|
64
|
+
if (fields.length >= 4 && fields.length <= 6) rows.push(fields);
|
|
65
|
+
}
|
|
66
|
+
const parsed = filtersRawSchema.safeParse(rows);
|
|
67
|
+
return parsed.success ? parsed.data : null;
|
|
68
|
+
}
|
|
69
|
+
const parseAsFilterTuples = createParser({
|
|
70
|
+
parse: (value) => parseFiltersParam(value),
|
|
71
|
+
serialize: (rows) => serializeFiltersParam(rows),
|
|
72
|
+
eq: (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
73
|
+
}).withDefault([]);
|
|
6
74
|
const parseAsGrouping = parseAsArrayOf(parseAsString).withDefault([]);
|
|
75
|
+
const NULLISH_METHODS = new Set([
|
|
76
|
+
"isEmpty",
|
|
77
|
+
"isNotEmpty",
|
|
78
|
+
"is_null",
|
|
79
|
+
"is_not_null"
|
|
80
|
+
]);
|
|
81
|
+
function encodeFilterValue(filter) {
|
|
82
|
+
if (NULLISH_METHODS.has(filter.method)) return typeof filter.value === "string" ? filter.value : "";
|
|
83
|
+
const { type, method, value } = filter;
|
|
84
|
+
switch (type) {
|
|
85
|
+
case "number": return String(value);
|
|
86
|
+
case "boolean": return value ? "true" : "false";
|
|
87
|
+
case "enum":
|
|
88
|
+
case "jsonArray":
|
|
89
|
+
if (method === "oneOf" && Array.isArray(value)) return JSON.stringify(value);
|
|
90
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
91
|
+
return String(value);
|
|
92
|
+
default: return String(value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function decodeFilterValue(type, method, valueStr) {
|
|
96
|
+
if (NULLISH_METHODS.has(method)) return valueStr;
|
|
97
|
+
switch (type) {
|
|
98
|
+
case "number": {
|
|
99
|
+
const n = Number.parseFloat(valueStr);
|
|
100
|
+
return Number.isNaN(n) ? 0 : n;
|
|
101
|
+
}
|
|
102
|
+
case "boolean": return valueStr === "true";
|
|
103
|
+
case "enum":
|
|
104
|
+
case "jsonArray":
|
|
105
|
+
if (method === "oneOf") try {
|
|
106
|
+
const parsed = JSON.parse(valueStr);
|
|
107
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
if (valueStr.startsWith("[")) try {
|
|
112
|
+
const parsed = JSON.parse(valueStr);
|
|
113
|
+
if (Array.isArray(parsed)) return parsed.map(String);
|
|
114
|
+
} catch {}
|
|
115
|
+
return valueStr;
|
|
116
|
+
default: return valueStr;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Builds URL tuple; omits optional tails when unused (endColumnId requires valueTo slot — may be ""). */
|
|
120
|
+
function encodeFilterToTuple(filter) {
|
|
121
|
+
const tuple = [
|
|
122
|
+
filter.columnId,
|
|
123
|
+
filter.type,
|
|
124
|
+
filter.method,
|
|
125
|
+
encodeFilterValue(filter)
|
|
126
|
+
];
|
|
127
|
+
const hasValueTo = filter.valueTo != null && filter.valueTo !== "";
|
|
128
|
+
const hasEndColumnId = filter.endColumnId != null && filter.endColumnId !== "";
|
|
129
|
+
if (hasValueTo && filter.valueTo != null) tuple.push(filter.valueTo);
|
|
130
|
+
if (hasEndColumnId && filter.endColumnId != null) {
|
|
131
|
+
if (!hasValueTo) tuple.push("");
|
|
132
|
+
tuple.push(filter.endColumnId);
|
|
133
|
+
}
|
|
134
|
+
return tuple;
|
|
135
|
+
}
|
|
136
|
+
function decodeTupleToFilter(parts) {
|
|
137
|
+
if (parts.length < 4 || parts.length > 6) return null;
|
|
138
|
+
const [columnId, typeStr, methodStr, valueStr, valueToStr, endColumnIdStr] = parts;
|
|
139
|
+
const typeParsed = filterSchema.shape.type.safeParse(typeStr);
|
|
140
|
+
const methodParsed = filterSchema.shape.method.safeParse(methodStr);
|
|
141
|
+
if (!typeParsed.success || !methodParsed.success) return null;
|
|
142
|
+
const type = typeParsed.data;
|
|
143
|
+
const method = methodParsed.data;
|
|
144
|
+
const candidate = {
|
|
145
|
+
columnId,
|
|
146
|
+
type,
|
|
147
|
+
method,
|
|
148
|
+
value: decodeFilterValue(type, method, valueStr)
|
|
149
|
+
};
|
|
150
|
+
if (valueToStr !== void 0) candidate.valueTo = valueToStr;
|
|
151
|
+
if (endColumnIdStr !== void 0) candidate.endColumnId = endColumnIdStr;
|
|
152
|
+
const parsed = filterSchema.safeParse(candidate);
|
|
153
|
+
return parsed.success ? parsed.data : null;
|
|
154
|
+
}
|
|
7
155
|
/**
|
|
8
156
|
* Hook to manage all query parameters via nuqs (URL query parameters)
|
|
9
|
-
* Manages: filters
|
|
157
|
+
* Manages: `f` (filters as tuple arrays), `q`, `s`/`o`, `p`/`l`, `g`, `r`
|
|
10
158
|
* Accepts an optional prefix to namespace params when multiple tables share a view.
|
|
11
159
|
*/
|
|
12
160
|
const useNuqsQueryParams = (prefix) => {
|
|
13
161
|
const k = (name) => prefix ? `${prefix}_${name}` : name;
|
|
14
|
-
const [sort, setSort] = useQueryState(k("
|
|
15
|
-
const [order, setOrder] = useQueryState(k("
|
|
16
|
-
const [page, setPage] = useQueryState(k("
|
|
17
|
-
const [limit, setLimit] = useQueryState(k("
|
|
162
|
+
const [sort, setSort] = useQueryState(k("s"), parseAsString.withDefault(""));
|
|
163
|
+
const [order, setOrder] = useQueryState(k("o"), parseAsStringLiteral(["asc", "desc"]));
|
|
164
|
+
const [page, setPage] = useQueryState(k("p"), parseAsInteger.withDefault(1));
|
|
165
|
+
const [limit, setLimit] = useQueryState(k("l"), parseAsInteger.withDefault(10));
|
|
18
166
|
const [qRaw, setQRaw] = useQueryState(k("q"), parseAsString);
|
|
19
|
-
const [
|
|
167
|
+
const [filtersRaw, setFiltersRaw] = useQueryState(k("f"), parseAsFilterTuples);
|
|
168
|
+
const filters = useMemo(() => {
|
|
169
|
+
const decoded = filtersRaw.map((row) => decodeTupleToFilter(row)).filter((f) => f != null);
|
|
170
|
+
const parsed = filtersSchema.safeParse(decoded);
|
|
171
|
+
return parsed.success ? parsed.data : [];
|
|
172
|
+
}, [filtersRaw]);
|
|
173
|
+
const setFilters = useCallback((next) => {
|
|
174
|
+
setFiltersRaw(next.length === 0 ? null : next.map(encodeFilterToTuple));
|
|
175
|
+
}, [setFiltersRaw]);
|
|
20
176
|
const setQ = useCallback((value) => {
|
|
21
177
|
setQRaw(value === "" || value == null ? null : value);
|
|
22
178
|
}, [setQRaw]);
|
|
23
|
-
const [granularity, setGranularity] = useQueryState(k("
|
|
179
|
+
const [granularity, setGranularity] = useQueryState(k("r"), parseAsStringLiteral([
|
|
24
180
|
"daily",
|
|
25
181
|
"weekly",
|
|
26
182
|
"monthly",
|
|
27
183
|
"yearly"
|
|
28
184
|
]).withDefault("daily"));
|
|
29
|
-
const [groupingRaw, setGroupingRaw] = useQueryState(k("
|
|
185
|
+
const [groupingRaw, setGroupingRaw] = useQueryState(k("g"), parseAsGrouping);
|
|
30
186
|
const grouping = groupingRaw;
|
|
31
187
|
const setGrouping = useCallback((updater) => {
|
|
32
188
|
const next = typeof updater === "function" ? updater(groupingRaw) : updater;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useNuqsQueryParams.mjs","names":[],"sources":["../../../../../src/modules/table/hooks/useNuqsQueryParams.ts"],"sourcesContent":["import type { QueryFilters } from \"@m5kdev/commons/modules/schemas/query.schema\";\r\nimport { filtersSchema } from \"@m5kdev/commons/modules/schemas/query.schema\";\r\nimport type { GroupingState, PaginationState, SortingState, Updater } from \"@tanstack/react-table\";\r\nimport {\r\n parseAsArrayOf,\r\n parseAsInteger,\r\n parseAsJson,\r\n parseAsString,\r\n parseAsStringLiteral,\r\n useQueryState,\r\n} from \"nuqs\";\r\nimport { useCallback, useEffect, useMemo } from \"react\";\r\n\r\nconst parseAsFilters = parseAsJson<QueryFilters>((value) => filtersSchema.parse(value)).withDefault(\r\n []\r\n);\r\n\r\nconst parseAsGrouping = parseAsArrayOf(parseAsString).withDefault([]);\r\n\r\nexport type Order = \"asc\" | \"desc\";\r\n\r\nexport type Granularity = \"daily\" | \"weekly\" | \"monthly\" | \"yearly\";\r\n\r\nexport interface NuqsQueryParams {\r\n /** Global search string from URL (`q`); absent when not in URL. */\r\n q: string | null;\r\n setQ: (value: string | null) => void;\r\n filters?: QueryFilters;\r\n setFilters?: (filters: QueryFilters) => void;\r\n granularity?: Granularity;\r\n setGranularity?: (value: Granularity) => void;\r\n sort?: string;\r\n order?: Order | null;\r\n setSorting?: (updater: Updater<SortingState>) => void;\r\n sorting?: SortingState;\r\n page?: number;\r\n limit?: number;\r\n setPagination?: (updater: Updater<PaginationState>) => void;\r\n pagination?: PaginationState;\r\n grouping: GroupingState;\r\n setGrouping: (updater: Updater<GroupingState>) => void;\r\n}\r\n\r\n/**\r\n * Hook to manage all query parameters via nuqs (URL query parameters)\r\n * Manages: filters, q, sort, order, page, limit, groupBy, granularity\r\n * Accepts an optional prefix to namespace params when multiple tables share a view.\r\n */\r\nexport const useNuqsQueryParams = (prefix?: string): NuqsQueryParams => {\r\n const k = (name: string) => (prefix ? `${prefix}_${name}` : name);\r\n\r\n const [sort, setSort] = useQueryState<string>(k(\"sort\"), parseAsString.withDefault(\"\"));\r\n const [order, setOrder] = useQueryState<Order>(k(\"order\"), parseAsStringLiteral([\"asc\", \"desc\"]));\r\n const [page, setPage] = useQueryState<number>(k(\"page\"), parseAsInteger.withDefault(1));\r\n const [limit, setLimit] = useQueryState<number>(k(\"limit\"), parseAsInteger.withDefault(10));\r\n const [qRaw, setQRaw] = useQueryState(k(\"q\"), parseAsString);\r\n const [filters, setFilters] = useQueryState<QueryFilters>(k(\"filters\"), parseAsFilters);\r\n\r\n const setQ = useCallback(\r\n (value: string | null) => {\r\n void setQRaw(value === \"\" || value == null ? null : value);\r\n },\r\n [setQRaw]\r\n );\r\n const [granularity, setGranularity] = useQueryState<Granularity>(\r\n k(\"granularity\"),\r\n parseAsStringLiteral([\"daily\", \"weekly\", \"monthly\", \"yearly\"]).withDefault(\"daily\")\r\n );\r\n const [groupingRaw, setGroupingRaw] = useQueryState<string[]>(k(\"groupBy\"), parseAsGrouping);\r\n const grouping: GroupingState = groupingRaw;\r\n\r\n const setGrouping = useCallback(\r\n (updater: Updater<GroupingState>) => {\r\n const next = typeof updater === \"function\" ? updater(groupingRaw) : updater;\r\n setGroupingRaw(next.length > 0 ? next : null);\r\n },\r\n [groupingRaw, setGroupingRaw]\r\n );\r\n\r\n const sorting = useMemo(() => {\r\n if (!sort) {\r\n return [];\r\n }\r\n const effectiveOrder = order ?? \"asc\";\r\n return [{ id: sort, desc: effectiveOrder === \"desc\" }];\r\n }, [sort, order]);\r\n\r\n const pagination = useMemo(() => ({ pageIndex: page - 1, pageSize: limit }), [page, limit]);\r\n\r\n const setSorting = useCallback(\r\n (updater: Updater<SortingState>) => {\r\n // Build current sorting state from current values\r\n const currentSorting: SortingState = sort\r\n ? [{ id: sort, desc: (order ?? \"asc\") === \"desc\" }]\r\n : [];\r\n\r\n const next = typeof updater === \"function\" ? updater(currentSorting) : updater;\r\n const first = next[0];\r\n\r\n if (!first || !first.id) {\r\n setSort(\"\");\r\n setOrder(null);\r\n return;\r\n }\r\n\r\n setSort(first.id);\r\n setOrder(first.desc ? \"desc\" : \"asc\");\r\n },\r\n [sort, order, setSort, setOrder]\r\n );\r\n\r\n // Sync order to \"asc\" if sort is set but order is null (handles URL state inconsistencies)\r\n useEffect(() => {\r\n if (sort && !order) {\r\n setOrder(\"asc\");\r\n }\r\n }, [sort, order, setOrder]);\r\n\r\n // biome-ignore lint/correctness/useExhaustiveDependencies: dependent on page and limit only\r\n const setPagination = useCallback(\r\n (updater: Updater<PaginationState>) => {\r\n // TanStack Table uses 0-based indexing, but URL uses 1-based\r\n const currentState = { pageIndex: page - 1, pageSize: limit };\r\n const next = typeof updater === \"function\" ? updater(currentState) : updater;\r\n // Convert back to 1-based for URL\r\n setPage((next.pageIndex ?? 0) + 1);\r\n setLimit(next.pageSize ?? 10);\r\n },\r\n [page, limit]\r\n );\r\n\r\n return useMemo(\r\n () => ({\r\n q: qRaw,\r\n setQ,\r\n filters,\r\n setFilters,\r\n granularity,\r\n setGranularity,\r\n sort,\r\n order,\r\n setSorting,\r\n sorting,\r\n page,\r\n limit,\r\n setPagination,\r\n pagination,\r\n grouping,\r\n setGrouping,\r\n }),\r\n [\r\n qRaw,\r\n setQ,\r\n filters,\r\n setFilters,\r\n granularity,\r\n setGranularity,\r\n sort,\r\n order,\r\n setSorting,\r\n sorting,\r\n page,\r\n limit,\r\n setPagination,\r\n pagination,\r\n grouping,\r\n setGrouping,\r\n ]\r\n );\r\n};\r\n"],"mappings":";;;;AAaA,MAAM,iBAAiB,aAA2B,UAAU,cAAc,MAAM,MAAM,CAAC,CAAC,YACtF,EAAE,CACH;AAED,MAAM,kBAAkB,eAAe,cAAc,CAAC,YAAY,EAAE,CAAC;;;;;;AA+BrE,MAAa,sBAAsB,WAAqC;CACtE,MAAM,KAAK,SAAkB,SAAS,GAAG,OAAO,GAAG,SAAS;CAE5D,MAAM,CAAC,MAAM,WAAW,cAAsB,EAAE,OAAO,EAAE,cAAc,YAAY,GAAG,CAAC;CACvF,MAAM,CAAC,OAAO,YAAY,cAAqB,EAAE,QAAQ,EAAE,qBAAqB,CAAC,OAAO,OAAO,CAAC,CAAC;CACjG,MAAM,CAAC,MAAM,WAAW,cAAsB,EAAE,OAAO,EAAE,eAAe,YAAY,EAAE,CAAC;CACvF,MAAM,CAAC,OAAO,YAAY,cAAsB,EAAE,QAAQ,EAAE,eAAe,YAAY,GAAG,CAAC;CAC3F,MAAM,CAAC,MAAM,WAAW,cAAc,EAAE,IAAI,EAAE,cAAc;CAC5D,MAAM,CAAC,SAAS,cAAc,cAA4B,EAAE,UAAU,EAAE,eAAe;CAEvF,MAAM,OAAO,aACV,UAAyB;AACnB,UAAQ,UAAU,MAAM,SAAS,OAAO,OAAO,MAAM;IAE5D,CAAC,QAAQ,CACV;CACD,MAAM,CAAC,aAAa,kBAAkB,cACpC,EAAE,cAAc,EAChB,qBAAqB;EAAC;EAAS;EAAU;EAAW;EAAS,CAAC,CAAC,YAAY,QAAQ,CACpF;CACD,MAAM,CAAC,aAAa,kBAAkB,cAAwB,EAAE,UAAU,EAAE,gBAAgB;CAC5F,MAAM,WAA0B;CAEhC,MAAM,cAAc,aACjB,YAAoC;EACnC,MAAM,OAAO,OAAO,YAAY,aAAa,QAAQ,YAAY,GAAG;AACpE,iBAAe,KAAK,SAAS,IAAI,OAAO,KAAK;IAE/C,CAAC,aAAa,eAAe,CAC9B;CAED,MAAM,UAAU,cAAc;AAC5B,MAAI,CAAC,KACH,QAAO,EAAE;AAGX,SAAO,CAAC;GAAE,IAAI;GAAM,OADG,SAAS,WACa;GAAQ,CAAC;IACrD,CAAC,MAAM,MAAM,CAAC;CAEjB,MAAM,aAAa,eAAe;EAAE,WAAW,OAAO;EAAG,UAAU;EAAO,GAAG,CAAC,MAAM,MAAM,CAAC;CAE3F,MAAM,aAAa,aAChB,YAAmC;EAOlC,MAAM,SADO,OAAO,YAAY,aAAa,QAJR,OACjC,CAAC;GAAE,IAAI;GAAM,OAAO,SAAS,WAAW;GAAQ,CAAC,GACjD,EAAE,CAE8D,GAAG,SACpD;AAEnB,MAAI,CAAC,SAAS,CAAC,MAAM,IAAI;AACvB,WAAQ,GAAG;AACX,YAAS,KAAK;AACd;;AAGF,UAAQ,MAAM,GAAG;AACjB,WAAS,MAAM,OAAO,SAAS,MAAM;IAEvC;EAAC;EAAM;EAAO;EAAS;EAAS,CACjC;AAGD,iBAAgB;AACd,MAAI,QAAQ,CAAC,MACX,UAAS,MAAM;IAEhB;EAAC;EAAM;EAAO;EAAS,CAAC;CAG3B,MAAM,gBAAgB,aACnB,YAAsC;EAErC,MAAM,eAAe;GAAE,WAAW,OAAO;GAAG,UAAU;GAAO;EAC7D,MAAM,OAAO,OAAO,YAAY,aAAa,QAAQ,aAAa,GAAG;AAErE,WAAS,KAAK,aAAa,KAAK,EAAE;AAClC,WAAS,KAAK,YAAY,GAAG;IAE/B,CAAC,MAAM,MAAM,CACd;AAED,QAAO,eACE;EACL,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF"}
|
|
1
|
+
{"version":3,"file":"useNuqsQueryParams.mjs","names":[],"sources":["../../../../../src/modules/table/hooks/useNuqsQueryParams.ts"],"sourcesContent":["import type { QueryFilter, QueryFilters } from \"@m5kdev/commons/modules/schemas/query.schema\";\r\nimport { filterSchema, filtersSchema } from \"@m5kdev/commons/modules/schemas/query.schema\";\r\nimport type { GroupingState, PaginationState, SortingState, Updater } from \"@tanstack/react-table\";\r\nimport {\r\n createParser,\r\n parseAsArrayOf,\r\n parseAsInteger,\r\n parseAsString,\r\n parseAsStringLiteral,\r\n useQueryState,\r\n} from \"nuqs\";\r\nimport { useCallback, useEffect, useMemo } from \"react\";\r\nimport { z } from \"zod\";\r\n\r\n/** Each row: [columnId, type, method, value, valueTo?, endColumnId?] — validated after parse */\r\nconst filtersRawSchema = z.array(z.array(z.string()).min(4).max(6));\r\n\r\n/** Escape `\\`, `|`, `;` so we can split filters without JSON quotes/brackets. */\r\nfunction escapeFilterField(raw: string): string {\r\n return raw.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\|/g, \"\\\\|\").replace(/;/g, \"\\\\;\");\r\n}\r\n\r\nfunction unescapeFilterField(encoded: string): string {\r\n let out = \"\";\r\n for (let i = 0; i < encoded.length; i++) {\r\n const c = encoded[i];\r\n if (c === \"\\\\\" && i + 1 < encoded.length) {\r\n const next = encoded[i + 1];\r\n if (next === \"\\\\\" || next === \"|\" || next === \";\") {\r\n out += next;\r\n i++;\r\n continue;\r\n }\r\n }\r\n out += c;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Split on `delimiter` only when not escaped as `\\` + delimiter.\r\n */\r\nfunction splitByEscapedDelimiter(s: string, delimiter: string): string[] {\r\n if (delimiter.length !== 1) {\r\n throw new Error(\"splitByEscapedDelimiter expects a single-character delimiter\");\r\n }\r\n const parts: string[] = [];\r\n let buf = \"\";\r\n for (let i = 0; i < s.length; i++) {\r\n const c = s[i];\r\n if (c === \"\\\\\" && i + 1 < s.length) {\r\n buf += c + s[i + 1];\r\n i++;\r\n continue;\r\n }\r\n if (c === delimiter) {\r\n parts.push(unescapeFilterField(buf));\r\n buf = \"\";\r\n continue;\r\n }\r\n buf += c;\r\n }\r\n parts.push(unescapeFilterField(buf));\r\n return parts;\r\n}\r\n\r\nfunction serializeFiltersParam(rows: string[][]): string {\r\n if (rows.length === 0) {\r\n return \"\";\r\n }\r\n return rows.map((row) => row.map(escapeFilterField).join(\"|\")).join(\";\");\r\n}\r\n\r\nfunction parseFiltersParam(value: string): string[][] | null {\r\n const trimmed = value.trim();\r\n if (trimmed === \"\") {\r\n return [];\r\n }\r\n const rowStrings = splitByEscapedDelimiter(trimmed, \";\");\r\n const rows: string[][] = [];\r\n for (const row of rowStrings) {\r\n if (row === \"\") {\r\n continue;\r\n }\r\n const fields = splitByEscapedDelimiter(row, \"|\");\r\n if (fields.length >= 4 && fields.length <= 6) {\r\n rows.push(fields);\r\n }\r\n }\r\n const parsed = filtersRawSchema.safeParse(rows);\r\n return parsed.success ? parsed.data : null;\r\n}\r\n\r\nconst parseAsFilterTuples = createParser<string[][]>({\r\n parse: (value) => parseFiltersParam(value),\r\n serialize: (rows) => serializeFiltersParam(rows),\r\n eq: (a, b) => JSON.stringify(a) === JSON.stringify(b),\r\n}).withDefault([]);\r\n\r\nconst parseAsGrouping = parseAsArrayOf(parseAsString).withDefault([]);\r\n\r\nconst NULLISH_METHODS: ReadonlySet<QueryFilter[\"method\"]> = new Set([\r\n \"isEmpty\",\r\n \"isNotEmpty\",\r\n \"is_null\",\r\n \"is_not_null\",\r\n]);\r\n\r\nfunction encodeFilterValue(filter: QueryFilter): string {\r\n if (NULLISH_METHODS.has(filter.method)) {\r\n return typeof filter.value === \"string\" ? filter.value : \"\";\r\n }\r\n const { type, method, value } = filter;\r\n switch (type) {\r\n case \"number\":\r\n return String(value as number);\r\n case \"boolean\":\r\n return (value as boolean) ? \"true\" : \"false\";\r\n case \"enum\":\r\n case \"jsonArray\":\r\n if (method === \"oneOf\" && Array.isArray(value)) {\r\n return JSON.stringify(value);\r\n }\r\n if (Array.isArray(value)) {\r\n return JSON.stringify(value);\r\n }\r\n return String(value);\r\n default:\r\n return String(value);\r\n }\r\n}\r\n\r\nfunction decodeFilterValue(\r\n type: QueryFilter[\"type\"],\r\n method: QueryFilter[\"method\"],\r\n valueStr: string\r\n): QueryFilter[\"value\"] {\r\n if (NULLISH_METHODS.has(method)) {\r\n return valueStr;\r\n }\r\n switch (type) {\r\n case \"number\": {\r\n const n = Number.parseFloat(valueStr);\r\n return Number.isNaN(n) ? 0 : n;\r\n }\r\n case \"boolean\":\r\n return valueStr === \"true\";\r\n case \"enum\":\r\n case \"jsonArray\":\r\n if (method === \"oneOf\") {\r\n try {\r\n const parsed = JSON.parse(valueStr) as unknown;\r\n return Array.isArray(parsed) ? parsed.map(String) : [];\r\n } catch {\r\n return [];\r\n }\r\n }\r\n if (valueStr.startsWith(\"[\")) {\r\n try {\r\n const parsed = JSON.parse(valueStr) as unknown;\r\n if (Array.isArray(parsed)) {\r\n return parsed.map(String);\r\n }\r\n } catch {\r\n /* use raw string */\r\n }\r\n }\r\n return valueStr;\r\n default:\r\n return valueStr;\r\n }\r\n}\r\n\r\n/** Builds URL tuple; omits optional tails when unused (endColumnId requires valueTo slot — may be \"\"). */\r\nfunction encodeFilterToTuple(filter: QueryFilter): string[] {\r\n const tuple: string[] = [filter.columnId, filter.type, filter.method, encodeFilterValue(filter)];\r\n const hasValueTo = filter.valueTo != null && filter.valueTo !== \"\";\r\n const hasEndColumnId = filter.endColumnId != null && filter.endColumnId !== \"\";\r\n\r\n if (hasValueTo && filter.valueTo != null) {\r\n tuple.push(filter.valueTo);\r\n }\r\n\r\n if (hasEndColumnId && filter.endColumnId != null) {\r\n if (!hasValueTo) {\r\n tuple.push(\"\");\r\n }\r\n tuple.push(filter.endColumnId);\r\n }\r\n\r\n return tuple;\r\n}\r\n\r\nfunction decodeTupleToFilter(parts: readonly string[]): QueryFilter | null {\r\n if (parts.length < 4 || parts.length > 6) {\r\n return null;\r\n }\r\n\r\n const [columnId, typeStr, methodStr, valueStr, valueToStr, endColumnIdStr] = parts;\r\n\r\n const typeParsed = filterSchema.shape.type.safeParse(typeStr);\r\n const methodParsed = filterSchema.shape.method.safeParse(methodStr);\r\n if (!typeParsed.success || !methodParsed.success) {\r\n return null;\r\n }\r\n\r\n const type = typeParsed.data;\r\n const method = methodParsed.data;\r\n const value = decodeFilterValue(type, method, valueStr);\r\n\r\n const candidate: QueryFilter = {\r\n columnId,\r\n type,\r\n method,\r\n value,\r\n };\r\n\r\n if (valueToStr !== undefined) {\r\n candidate.valueTo = valueToStr;\r\n }\r\n if (endColumnIdStr !== undefined) {\r\n candidate.endColumnId = endColumnIdStr;\r\n }\r\n\r\n const parsed = filterSchema.safeParse(candidate);\r\n return parsed.success ? parsed.data : null;\r\n}\r\n\r\nexport type Order = \"asc\" | \"desc\";\r\n\r\nexport type Granularity = \"daily\" | \"weekly\" | \"monthly\" | \"yearly\";\r\n\r\nexport interface NuqsQueryParams {\r\n /** Global search string from URL (`q`); absent when not in URL. */\r\n q: string | null;\r\n setQ: (value: string | null) => void;\r\n filters?: QueryFilters;\r\n setFilters?: (filters: QueryFilters) => void;\r\n granularity?: Granularity;\r\n setGranularity?: (value: Granularity) => void;\r\n sort?: string;\r\n order?: Order | null;\r\n setSorting?: (updater: Updater<SortingState>) => void;\r\n sorting?: SortingState;\r\n page?: number;\r\n limit?: number;\r\n setPagination?: (updater: Updater<PaginationState>) => void;\r\n pagination?: PaginationState;\r\n grouping: GroupingState;\r\n setGrouping: (updater: Updater<GroupingState>) => void;\r\n}\r\n\r\n/**\r\n * Hook to manage all query parameters via nuqs (URL query parameters)\r\n * Manages: `f` (filters as tuple arrays), `q`, `s`/`o`, `p`/`l`, `g`, `r`\r\n * Accepts an optional prefix to namespace params when multiple tables share a view.\r\n */\r\nexport const useNuqsQueryParams = (prefix?: string): NuqsQueryParams => {\r\n const k = (name: string) => (prefix ? `${prefix}_${name}` : name);\r\n\r\n const [sort, setSort] = useQueryState<string>(k(\"s\"), parseAsString.withDefault(\"\"));\r\n const [order, setOrder] = useQueryState<Order>(k(\"o\"), parseAsStringLiteral([\"asc\", \"desc\"]));\r\n const [page, setPage] = useQueryState<number>(k(\"p\"), parseAsInteger.withDefault(1));\r\n const [limit, setLimit] = useQueryState<number>(k(\"l\"), parseAsInteger.withDefault(10));\r\n const [qRaw, setQRaw] = useQueryState(k(\"q\"), parseAsString);\r\n const [filtersRaw, setFiltersRaw] = useQueryState<string[][]>(k(\"f\"), parseAsFilterTuples);\r\n\r\n const filters = useMemo((): QueryFilters => {\r\n const decoded = filtersRaw\r\n .map((row) => decodeTupleToFilter(row))\r\n .filter((f): f is QueryFilter => f != null);\r\n const parsed = filtersSchema.safeParse(decoded);\r\n return parsed.success ? parsed.data : [];\r\n }, [filtersRaw]);\r\n\r\n const setFilters = useCallback(\r\n (next: QueryFilters) => {\r\n void setFiltersRaw(next.length === 0 ? null : next.map(encodeFilterToTuple));\r\n },\r\n [setFiltersRaw]\r\n );\r\n\r\n const setQ = useCallback(\r\n (value: string | null) => {\r\n void setQRaw(value === \"\" || value == null ? null : value);\r\n },\r\n [setQRaw]\r\n );\r\n const [granularity, setGranularity] = useQueryState<Granularity>(\r\n k(\"r\"),\r\n parseAsStringLiteral([\"daily\", \"weekly\", \"monthly\", \"yearly\"]).withDefault(\"daily\")\r\n );\r\n const [groupingRaw, setGroupingRaw] = useQueryState<string[]>(k(\"g\"), parseAsGrouping);\r\n const grouping: GroupingState = groupingRaw;\r\n\r\n const setGrouping = useCallback(\r\n (updater: Updater<GroupingState>) => {\r\n const next = typeof updater === \"function\" ? updater(groupingRaw) : updater;\r\n setGroupingRaw(next.length > 0 ? next : null);\r\n },\r\n [groupingRaw, setGroupingRaw]\r\n );\r\n\r\n const sorting = useMemo(() => {\r\n if (!sort) {\r\n return [];\r\n }\r\n const effectiveOrder = order ?? \"asc\";\r\n return [{ id: sort, desc: effectiveOrder === \"desc\" }];\r\n }, [sort, order]);\r\n\r\n const pagination = useMemo(() => ({ pageIndex: page - 1, pageSize: limit }), [page, limit]);\r\n\r\n const setSorting = useCallback(\r\n (updater: Updater<SortingState>) => {\r\n const currentSorting: SortingState = sort\r\n ? [{ id: sort, desc: (order ?? \"asc\") === \"desc\" }]\r\n : [];\r\n\r\n const next = typeof updater === \"function\" ? updater(currentSorting) : updater;\r\n const first = next[0];\r\n\r\n if (!first || !first.id) {\r\n setSort(\"\");\r\n setOrder(null);\r\n return;\r\n }\r\n\r\n setSort(first.id);\r\n setOrder(first.desc ? \"desc\" : \"asc\");\r\n },\r\n [sort, order, setSort, setOrder]\r\n );\r\n\r\n useEffect(() => {\r\n if (sort && !order) {\r\n setOrder(\"asc\");\r\n }\r\n }, [sort, order, setOrder]);\r\n\r\n // biome-ignore lint/correctness/useExhaustiveDependencies: dependent on page and limit only\r\n const setPagination = useCallback(\r\n (updater: Updater<PaginationState>) => {\r\n const currentState = { pageIndex: page - 1, pageSize: limit };\r\n const next = typeof updater === \"function\" ? updater(currentState) : updater;\r\n setPage((next.pageIndex ?? 0) + 1);\r\n setLimit(next.pageSize ?? 10);\r\n },\r\n [page, limit]\r\n );\r\n\r\n return useMemo(\r\n () => ({\r\n q: qRaw,\r\n setQ,\r\n filters,\r\n setFilters,\r\n granularity,\r\n setGranularity,\r\n sort,\r\n order,\r\n setSorting,\r\n sorting,\r\n page,\r\n limit,\r\n setPagination,\r\n pagination,\r\n grouping,\r\n setGrouping,\r\n }),\r\n [\r\n qRaw,\r\n setQ,\r\n filters,\r\n setFilters,\r\n granularity,\r\n setGranularity,\r\n sort,\r\n order,\r\n setSorting,\r\n sorting,\r\n page,\r\n limit,\r\n setPagination,\r\n pagination,\r\n grouping,\r\n setGrouping,\r\n ]\r\n );\r\n};\r\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m5kdev/frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "GPL-3.0-only",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"react": "19.2.1",
|
|
25
25
|
"react-dom": "19.2.1",
|
|
26
26
|
"zod": "4.2.1",
|
|
27
|
-
"@m5kdev/commons": "0.
|
|
27
|
+
"@m5kdev/commons": "0.9.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@heroui/react": "2.8.10",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"tsdown": "0.21.7",
|
|
37
37
|
"typescript": "5.9.2",
|
|
38
38
|
"vite": "7.0.4",
|
|
39
|
-
"@m5kdev/config": "0.
|
|
40
|
-
"@m5kdev/backend": "0.
|
|
39
|
+
"@m5kdev/config": "0.9.0",
|
|
40
|
+
"@m5kdev/backend": "0.9.0"
|
|
41
41
|
},
|
|
42
42
|
"exports": {
|
|
43
43
|
"./modules/auth/components/*": {
|