@dnax/ui 0.0.54 → 0.0.56
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/components/QAutocomplete.vue +11 -5
- package/components/QSpreadsheet.vue +121 -86
- package/lib/select.ts +3 -2
- package/lib/spreadsheetOptions.test.ts +84 -0
- package/lib/spreadsheetOptions.ts +70 -0
- package/lib/spreadsheetValidation.test.ts +106 -0
- package/lib/spreadsheetValidation.ts +102 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { cn } from "../lib/utils"
|
|
|
9
9
|
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
10
10
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
11
11
|
import { useOverlayBack } from "../lib/overlayBack"
|
|
12
|
+
import { optionsOf } from "../lib/select"
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
/** Valeur sélectionnée */
|
|
@@ -230,18 +231,23 @@ const getLabel = (opt: any): string => {
|
|
|
230
231
|
return v === undefined || v === null ? "" : String(v)
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
// `options` peut arriver `false` (v-if/&&, config non chargée) : sans cette coercition,
|
|
235
|
+
// `.find` / `.filter` / `.some` jetteraient et le popup cesserait de se rendre
|
|
236
|
+
// (même classe de bug que `q-select multiple` — cf. `.memory/knowledges.md`).
|
|
237
|
+
const optionList = computed<any[]>(() => optionsOf(props.options))
|
|
238
|
+
|
|
233
239
|
const selectedLabel = computed(() => {
|
|
234
240
|
if (props.modelValue === undefined || props.modelValue === null) return ""
|
|
235
|
-
const opt =
|
|
241
|
+
const opt = optionList.value.find((o) => getValue(o) === props.modelValue)
|
|
236
242
|
return opt ? getLabel(opt) : String(props.modelValue)
|
|
237
243
|
})
|
|
238
244
|
|
|
239
245
|
// — Filtre client (sauté en mode serveur @filter) —
|
|
240
246
|
const filtered = computed(() => {
|
|
241
|
-
if (hasFilter.value) return
|
|
247
|
+
if (hasFilter.value) return optionList.value
|
|
242
248
|
const q = query.value.trim().toLowerCase()
|
|
243
|
-
if (!q) return
|
|
244
|
-
return
|
|
249
|
+
if (!q) return optionList.value
|
|
250
|
+
return optionList.value.filter((o) => getLabel(o).toLowerCase().includes(q))
|
|
245
251
|
})
|
|
246
252
|
|
|
247
253
|
// — Affichage du champ —
|
|
@@ -323,7 +329,7 @@ const commit = () => {
|
|
|
323
329
|
const q = query.value.trim()
|
|
324
330
|
if (!q) return
|
|
325
331
|
const label = q.toLowerCase()
|
|
326
|
-
const matches =
|
|
332
|
+
const matches = optionList.value.some((o) => getLabel(o).toLowerCase() === label)
|
|
327
333
|
if (!matches) {
|
|
328
334
|
query.value = ""
|
|
329
335
|
emit("update:inputValue", "")
|
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
// Suivi des modifications : `v-model:dirty` (état) + `v-model:changes` (delta lignes / feuilles
|
|
9
9
|
// ajoutées, modifiées, supprimées), calculé en comparant le document à une **référence**
|
|
10
10
|
// (`lib/spreadsheetChanges.ts`).
|
|
11
|
+
import type {
|
|
12
|
+
QSpreadsheetCellOption,
|
|
13
|
+
QSpreadsheetOptionAccessor,
|
|
14
|
+
} from "../lib/spreadsheetOptions"
|
|
15
|
+
import type {
|
|
16
|
+
QSpreadsheetValidation,
|
|
17
|
+
QSpreadsheetValidationRule,
|
|
18
|
+
} from "../lib/spreadsheetValidation"
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
QSpreadsheetCellOption,
|
|
22
|
+
QSpreadsheetOptionAccessor,
|
|
23
|
+
} from "../lib/spreadsheetOptions"
|
|
24
|
+
export type {
|
|
25
|
+
QSpreadsheetValidation,
|
|
26
|
+
QSpreadsheetValidationRule,
|
|
27
|
+
} from "../lib/spreadsheetValidation"
|
|
28
|
+
|
|
11
29
|
export type QSpreadsheetCellType =
|
|
12
30
|
| "string"
|
|
13
31
|
| "text"
|
|
@@ -21,18 +39,9 @@ export type QSpreadsheetCellType =
|
|
|
21
39
|
| "select"
|
|
22
40
|
| "multiselect"
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Libellé affiché. Les nombres sont acceptés (`{ value: 1, label: 1 }` pour une
|
|
29
|
-
* note, un niveau…) : le composant les normalise en string partout où il fait des
|
|
30
|
-
* opérations de chaîne (recherche, filtre, tri, éditeur).
|
|
31
|
-
*/
|
|
32
|
-
label: string | number
|
|
33
|
-
/** Couleur du badge (token ou hex, ex. "positive", "#22c55e") */
|
|
34
|
-
color?: string
|
|
35
|
-
}
|
|
42
|
+
// `QSpreadsheetCellOption` (option **normalisée**) et `QSpreadsheetOptionAccessor` vivent dans
|
|
43
|
+
// `lib/spreadsheetOptions.ts` — source unique, testée hors navigateur — et sont ré-exportés en
|
|
44
|
+
// tête de ce bloc.
|
|
36
45
|
|
|
37
46
|
export interface QSpreadsheetColumn {
|
|
38
47
|
/** Clé de la propriété dans la ligne */
|
|
@@ -48,12 +57,27 @@ export interface QSpreadsheetColumn {
|
|
|
48
57
|
type?: QSpreadsheetCellType
|
|
49
58
|
/** La cellule est éditable (Entrée / double-clic / frappe) */
|
|
50
59
|
editable?: boolean
|
|
51
|
-
/**
|
|
52
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Pour type `select` / `multiselect` : les options. Forme par défaut
|
|
62
|
+
* `{ value, label, color? }` — ou **n'importe quelle forme d'objet** (ex. `[{ _id, name }]`
|
|
63
|
+
* renvoyé par une API) dès que `optionLabel` / `optionValue` sont fournis.
|
|
64
|
+
*/
|
|
65
|
+
options?: any[]
|
|
66
|
+
/**
|
|
67
|
+
* Pour type `select` / `multiselect` : clé (ou fonction) du **libellé** dans une option —
|
|
68
|
+
* défaut `"label"`. Ex. `option-label="name"`. Vocabulaire de `<q-select>`.
|
|
69
|
+
*/
|
|
70
|
+
optionLabel?: QSpreadsheetOptionAccessor
|
|
71
|
+
/**
|
|
72
|
+
* Pour type `select` / `multiselect` : clé (ou fonction) de la **valeur stockée** dans la
|
|
73
|
+
* cellule — défaut `"value"`. Doit être **primitive** (string / number / bool) et unique.
|
|
74
|
+
* Ex. `option-value="_id"`.
|
|
75
|
+
*/
|
|
76
|
+
optionValue?: QSpreadsheetOptionAccessor
|
|
53
77
|
/** Pour type "select" / "multiselect" : rend les options actives en badge coloré (sinon texte) */
|
|
54
78
|
chip?: boolean
|
|
55
|
-
/** Validation à la saisie : { min, max,
|
|
56
|
-
validation?:
|
|
79
|
+
/** Validation à la saisie : objet `{ min, max, … }` ou expression ArkType (`"number < 4"`) */
|
|
80
|
+
validation?: QSpreadsheetValidationRule
|
|
57
81
|
/** Formatteur de la valeur affichée */
|
|
58
82
|
format?: (val: any, row: Record<string, any>) => any
|
|
59
83
|
/** Classe CSS de la cellule (défaut : ) */
|
|
@@ -64,24 +88,9 @@ export interface QSpreadsheetColumn {
|
|
|
64
88
|
headerStyle?: string
|
|
65
89
|
}
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/** Valeur maximale (types numériques / entiers) */
|
|
71
|
-
max?: number
|
|
72
|
-
/** Exige un entier (si présent, valide les entiers) */
|
|
73
|
-
integer?: boolean
|
|
74
|
-
/** Expression régulière validée contre le texte */
|
|
75
|
-
pattern?: string
|
|
76
|
-
/** Champ obligatoire (vide refusé) */
|
|
77
|
-
required?: boolean
|
|
78
|
-
/** Valeurs autorisées (liste) */
|
|
79
|
-
list?: any[]
|
|
80
|
-
/** Message d'erreur affiché (sinon message générique) */
|
|
81
|
-
message?: string
|
|
82
|
-
/** Schéma ArkType (string) — ex. "number.integer & >= 0 & <= 100" (optionnel) */
|
|
83
|
-
schema?: string
|
|
84
|
-
}
|
|
91
|
+
// `QSpreadsheetValidation` et `QSpreadsheetValidationRule` vivent dans
|
|
92
|
+
// `lib/spreadsheetValidation.ts` (source unique, testée hors navigateur) et sont
|
|
93
|
+
// ré-exportées en tête de ce bloc.
|
|
85
94
|
|
|
86
95
|
/** Validation par plage de cellules (rows/cols 0-based) */
|
|
87
96
|
export interface QSpreadsheetRangeValidator {
|
|
@@ -89,7 +98,7 @@ export interface QSpreadsheetRangeValidator {
|
|
|
89
98
|
c0: number
|
|
90
99
|
r1: number
|
|
91
100
|
c1: number
|
|
92
|
-
validation:
|
|
101
|
+
validation: QSpreadsheetValidationRule
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
export interface QSpreadsheetSelection {
|
|
@@ -161,6 +170,8 @@ import {
|
|
|
161
170
|
deriveCols,
|
|
162
171
|
parseCsv,
|
|
163
172
|
} from "../lib/spreadsheet"
|
|
173
|
+
import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
|
|
174
|
+
import { normalizeCellOptions } from "../lib/spreadsheetOptions"
|
|
164
175
|
import {
|
|
165
176
|
DEFAULT_ROW_KEY,
|
|
166
177
|
diffDocuments,
|
|
@@ -517,6 +528,38 @@ const colNameAt = (i: number) => cols.value[i]?.name ?? ""
|
|
|
517
528
|
const colOf = (name: string) => cols.value.find((c) => c.name === name)
|
|
518
529
|
|
|
519
530
|
// ─── Cellules à choix (select = une valeur, multiselect = un tableau de valeurs) ───
|
|
531
|
+
/**
|
|
532
|
+
* Options d'une colonne, **normalisées une seule fois** vers `{ value, label, color? }` —
|
|
533
|
+
* `optionLabel` / `optionValue` y sont appliqués (défauts `"label"` / `"value"`). C'est ce qui
|
|
534
|
+
* garantit que badge, info-bulle, éditeur, tri, filtre et copie/CSV lisent tous la même forme :
|
|
535
|
+
* un accesseur oublié quelque part donnerait un badge `_id` et un tooltip « Ada ».
|
|
536
|
+
* Mémoïsé par nom de colonne (le cache vit dans l'instance du composant).
|
|
537
|
+
*/
|
|
538
|
+
const optionsCache = new Map<
|
|
539
|
+
string,
|
|
540
|
+
{ opts: QSpreadsheetCellOption[]; options: unknown; label: unknown; value: unknown }
|
|
541
|
+
>()
|
|
542
|
+
const cellOptionsOf = (col: QSpreadsheetColumn | undefined): QSpreadsheetCellOption[] => {
|
|
543
|
+
if (!col) return []
|
|
544
|
+
const hit = optionsCache.get(col.name)
|
|
545
|
+
if (
|
|
546
|
+
hit &&
|
|
547
|
+
hit.options === col.options &&
|
|
548
|
+
hit.label === col.optionLabel &&
|
|
549
|
+
hit.value === col.optionValue
|
|
550
|
+
) {
|
|
551
|
+
return hit.opts
|
|
552
|
+
}
|
|
553
|
+
const opts = normalizeCellOptions(col.options, col.optionLabel, col.optionValue)
|
|
554
|
+
optionsCache.set(col.name, {
|
|
555
|
+
opts,
|
|
556
|
+
options: col.options,
|
|
557
|
+
label: col.optionLabel,
|
|
558
|
+
value: col.optionValue,
|
|
559
|
+
})
|
|
560
|
+
return opts
|
|
561
|
+
}
|
|
562
|
+
|
|
520
563
|
/**
|
|
521
564
|
* Valeurs d'une cellule à choix multiples, toujours sous forme de liste : un tableau est
|
|
522
565
|
* repris tel quel, une valeur nue (`null`, `"it"`…) devient `[valeur]` (ou `[]` si vide).
|
|
@@ -530,7 +573,7 @@ const multiValues = (raw: any): any[] => {
|
|
|
530
573
|
/** Libellés d'une cellule à choix multiples (le `value` brut si l'option est inconnue) */
|
|
531
574
|
const multiLabels = (col: QSpreadsheetColumn | undefined, raw: any): string[] =>
|
|
532
575
|
multiValues(raw).map((v) => {
|
|
533
|
-
const opt = col
|
|
576
|
+
const opt = cellOptionsOf(col).find((o) => o.value === v)
|
|
534
577
|
return String(opt?.label ?? v)
|
|
535
578
|
})
|
|
536
579
|
|
|
@@ -542,7 +585,7 @@ const choiceText = (col: QSpreadsheetColumn | undefined, raw: any): string => {
|
|
|
542
585
|
if (col?.type === "multiselect") return multiLabels(col, raw).join(", ")
|
|
543
586
|
if (isBlankValue(raw)) return ""
|
|
544
587
|
if (col?.type === "select") {
|
|
545
|
-
const opt = col.
|
|
588
|
+
const opt = cellOptionsOf(col).find((o) => o.value === raw)
|
|
546
589
|
if (opt?.label !== undefined) return String(opt.label)
|
|
547
590
|
}
|
|
548
591
|
return String(raw)
|
|
@@ -842,7 +885,7 @@ const startEdit = (row: number, column: string, initial?: string) => {
|
|
|
842
885
|
if (col?.editable === false || col?.type === "boolean") return
|
|
843
886
|
const raw = state.value[row]?.[column]
|
|
844
887
|
if (col?.type === "select") {
|
|
845
|
-
const opt = col.
|
|
888
|
+
const opt = cellOptionsOf(col).find((o) => o.value === raw)
|
|
846
889
|
// `label` peut être un nombre (options numériques) : `draft` alimente un
|
|
847
890
|
// <input>/<textarea> et sert à `.trim()` / `.toLowerCase()` → toujours une string.
|
|
848
891
|
draft.value = String(initial ?? opt?.label ?? "")
|
|
@@ -929,7 +972,9 @@ const coerceValue = (col: QSpreadsheetColumn | undefined, text: unknown, old: an
|
|
|
929
972
|
return raw === "true" ? true : raw === "false" ? false : old
|
|
930
973
|
}
|
|
931
974
|
if (col?.type === "select") {
|
|
932
|
-
const opt = col.
|
|
975
|
+
const opt = cellOptionsOf(col).find(
|
|
976
|
+
(o) => String(o.label ?? "") === raw || String(o.value) === raw,
|
|
977
|
+
)
|
|
933
978
|
return opt ? opt.value : old
|
|
934
979
|
}
|
|
935
980
|
// `multiselect` : collage / saisie « a, b » → tableau de valeurs d'option
|
|
@@ -940,7 +985,7 @@ const coerceValue = (col: QSpreadsheetColumn | undefined, text: unknown, old: an
|
|
|
940
985
|
.filter((s) => s !== "")
|
|
941
986
|
if (!parts.length) return null
|
|
942
987
|
return parts.map((part) => {
|
|
943
|
-
const opt = col.
|
|
988
|
+
const opt = cellOptionsOf(col).find(
|
|
944
989
|
(o) => String(o.label ?? "") === part || String(o.value) === part,
|
|
945
990
|
)
|
|
946
991
|
return opt ? opt.value : part
|
|
@@ -1641,7 +1686,7 @@ const selectOptions = computed<QSpreadsheetCellOption[]>(() => {
|
|
|
1641
1686
|
const col = colOf(editing.value.column)
|
|
1642
1687
|
if (col?.type !== "select" && col?.type !== "multiselect") return []
|
|
1643
1688
|
const q = String(draft.value ?? "").trim().toLowerCase()
|
|
1644
|
-
return (col
|
|
1689
|
+
return cellOptionsOf(col).filter(
|
|
1645
1690
|
(o) => !q || String(o.label ?? o.value ?? "").toLowerCase().includes(q),
|
|
1646
1691
|
)
|
|
1647
1692
|
})
|
|
@@ -1676,7 +1721,7 @@ const toggleMultiOption = (opt: QSpreadsheetCellOption) => {
|
|
|
1676
1721
|
const next = multiSelected(opt)
|
|
1677
1722
|
? current.filter((v) => v !== opt.value)
|
|
1678
1723
|
: [...current, opt.value]
|
|
1679
|
-
validateAndSet(row, column, old, next)
|
|
1724
|
+
void validateAndSet(row, column, old, next)
|
|
1680
1725
|
}
|
|
1681
1726
|
|
|
1682
1727
|
// ─── Barre de formule (fx) ───
|
|
@@ -2499,7 +2544,7 @@ const badgeStyle = (opt: QSpreadsheetCellOption) => {
|
|
|
2499
2544
|
|
|
2500
2545
|
const badgeOf = (col: QSpreadsheetColumn | undefined, val: any) => {
|
|
2501
2546
|
if (col?.type !== "select" || !col.chip) return undefined
|
|
2502
|
-
const opt = col.
|
|
2547
|
+
const opt = cellOptionsOf(col).find((o) => o.value === val)
|
|
2503
2548
|
if (!opt) return undefined
|
|
2504
2549
|
return { label: opt.label, style: badgeStyle(opt) }
|
|
2505
2550
|
}
|
|
@@ -2508,7 +2553,7 @@ const badgeOf = (col: QSpreadsheetColumn | undefined, val: any) => {
|
|
|
2508
2553
|
const multiBadges = (col: QSpreadsheetColumn | undefined, val: any) => {
|
|
2509
2554
|
if (col?.type !== "multiselect" || !col.chip) return []
|
|
2510
2555
|
return multiValues(val).map((v) => {
|
|
2511
|
-
const opt = col.
|
|
2556
|
+
const opt = cellOptionsOf(col).find((o) => o.value === v)
|
|
2512
2557
|
return {
|
|
2513
2558
|
label: String(opt?.label ?? v),
|
|
2514
2559
|
style: badgeStyle(opt ?? { value: v, label: String(v) }),
|
|
@@ -3854,60 +3899,50 @@ const condStyleOf = (r: number, c: number, name: string) => {
|
|
|
3854
3899
|
return Object.keys(st).length ? st : undefined
|
|
3855
3900
|
}
|
|
3856
3901
|
|
|
3857
|
-
// ════════ Validation de colonne (min / max / entier / pattern) ════════
|
|
3902
|
+
// ════════ Validation de colonne (min / max / entier / pattern / ArkType) ════════
|
|
3858
3903
|
const valErrors = ref<Record<string, string>>({})
|
|
3859
3904
|
const valKey = (r: number, name: string) => r + ":" + name
|
|
3905
|
+
|
|
3906
|
+
// ArkType : import **dynamique** (jamais dans le graphe du barrel) mémoïsé au niveau
|
|
3907
|
+
// module — une seule promesse pour toute la session, et non une par édition de cellule.
|
|
3908
|
+
let arkModule: Promise<any | null> | null = null
|
|
3909
|
+
const loadArk = (): Promise<any | null> => {
|
|
3910
|
+
arkModule ??= import("arktype")
|
|
3911
|
+
.then((m) => m as any)
|
|
3912
|
+
.catch(() => null)
|
|
3913
|
+
return arkModule
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
/**
|
|
3917
|
+
* Vérifie une expression ArkType. Une expression **invalide** (faute de syntaxe) ne doit
|
|
3918
|
+
* JAMAIS jeter : l'exception remonterait en rejet non géré et la cellule ne serait ni
|
|
3919
|
+
* validée ni commitée, sans aucun message (cf. `.memory/knowledges.md`).
|
|
3920
|
+
*/
|
|
3921
|
+
const arkCheck: ArkCheck = async (schema, value) => {
|
|
3922
|
+
const m = await loadArk()
|
|
3923
|
+
if (!m) return "ArkType unavailable"
|
|
3924
|
+
try {
|
|
3925
|
+
const problems = m.type(schema)(value)
|
|
3926
|
+
const p = Array.isArray(problems) ? problems[0] : undefined
|
|
3927
|
+
return p?.message ?? null
|
|
3928
|
+
} catch (e) {
|
|
3929
|
+
return `Invalid ArkType schema: ${(e as Error)?.message ?? String(e)}`
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
/** Validation d'une cellule : règles de la colonne, puis celles des plages qui la couvrent. */
|
|
3860
3934
|
const guardValidation = async (
|
|
3861
3935
|
row: number,
|
|
3862
3936
|
col: QSpreadsheetColumn | undefined,
|
|
3863
3937
|
value: any,
|
|
3864
3938
|
): Promise<string | null> => {
|
|
3865
|
-
|
|
3866
|
-
const arkCheck = async (schema: string): Promise<string | null> => {
|
|
3867
|
-
if (!ark)
|
|
3868
|
-
ark = import("arktype")
|
|
3869
|
-
.then((m) => m as any)
|
|
3870
|
-
.catch(() => null)
|
|
3871
|
-
const m = await ark
|
|
3872
|
-
if (!m) return "ArkType unavailable"
|
|
3873
|
-
const problems = m.type(schema)(value)
|
|
3874
|
-
const p = Array.isArray(problems) ? problems[0] : undefined
|
|
3875
|
-
return p?.message ?? null
|
|
3876
|
-
}
|
|
3877
|
-
const check = async (v: QSpreadsheetValidation | undefined): Promise<string | null> => {
|
|
3878
|
-
if (!v) return null
|
|
3879
|
-
const multi = Array.isArray(value)
|
|
3880
|
-
const blank =
|
|
3881
|
-
value === null || value === undefined || value === "" || (multi && value.length === 0)
|
|
3882
|
-
if (v.required && blank) return v.message ?? "Required"
|
|
3883
|
-
if (blank) return null // champ vide autorisé (sauf required)
|
|
3884
|
-
if (v.list) {
|
|
3885
|
-
// Choix multiples : chacune des valeurs doit être dans la liste
|
|
3886
|
-
const values = multi ? value : [value]
|
|
3887
|
-
if (values.some((x) => !v.list!.some((y) => String(y) === String(x))))
|
|
3888
|
-
return v.message ?? "Not in the allowed list"
|
|
3889
|
-
}
|
|
3890
|
-
if (v.integer && typeof value === "number" && !Number.isInteger(value))
|
|
3891
|
-
return v.message ?? "Integer required"
|
|
3892
|
-
if (typeof value === "number") {
|
|
3893
|
-
if (v.min !== undefined && value < v.min) return v.message ?? "Value below minimum (" + v.min + ")"
|
|
3894
|
-
if (v.max !== undefined && value > v.max) return v.message ?? "Value above maximum (" + v.max + ")"
|
|
3895
|
-
}
|
|
3896
|
-
if (v.pattern && typeof value === "string" && !new RegExp(v.pattern).test(value))
|
|
3897
|
-
return v.message ?? "Value does not match the required format"
|
|
3898
|
-
if (v.schema) {
|
|
3899
|
-
const e = await arkCheck(v.schema)
|
|
3900
|
-
if (e) return v.message ?? e
|
|
3901
|
-
}
|
|
3902
|
-
return null
|
|
3903
|
-
}
|
|
3904
|
-
const colErr = await check(col?.validation)
|
|
3939
|
+
const colErr = await checkValidation(col?.validation, value, arkCheck)
|
|
3905
3940
|
if (colErr) return colErr
|
|
3906
3941
|
if (props.validators?.length && col) {
|
|
3907
3942
|
const ci = colIndex(col.name)
|
|
3908
3943
|
for (const rv of props.validators) {
|
|
3909
3944
|
if (row >= rv.r0 && row <= rv.r1 && ci >= rv.c0 && ci <= rv.c1) {
|
|
3910
|
-
const e = await
|
|
3945
|
+
const e = await checkValidation(rv.validation, value, arkCheck)
|
|
3911
3946
|
if (e) return e
|
|
3912
3947
|
}
|
|
3913
3948
|
}
|
package/lib/select.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// Coercitions défensives de QSelect (`lib/select.ts`).
|
|
1
|
+
// Coercitions défensives des composants de sélection (QSelect, QAutocomplete) (`lib/select.ts`).
|
|
2
2
|
//
|
|
3
3
|
// Un `v-model` ou des `options` mal formés ne doivent **jamais** casser le rendu : un
|
|
4
4
|
// `list.map is not a function` dans un `computed` du template tue tout le popup, sans
|
|
5
5
|
// message utile. Cas réel : `<q-select multiple v-model="x">` avec `x` initialisé à
|
|
6
6
|
// `false` (défaut d'un `ref` booléen, retour d'un `&&`, config non chargée) — le garde
|
|
7
|
-
// `=== null`/`=== undefined` ne l'attrape pas, puis `false.map(...)` jette.
|
|
7
|
+
// `=== null`/`=== undefined` ne l'attrape pas, puis `false.map(...)` jette. Idem
|
|
8
|
+
// `:options` arrivant `false` (`props.options.map` / `.filter` / `.find`).
|
|
8
9
|
//
|
|
9
10
|
// Pur (aucun DOM) → testable hors navigateur, comme `lib/pagePadding.ts`.
|
|
10
11
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Tests unitaires de la normalisation des options de colonne (`lib/spreadsheetOptions.ts`).
|
|
2
|
+
// Cas visé : des données d'API `[{ _id, name }]` (ou `{ _id, name, value }`) utilisées
|
|
3
|
+
// directement via `optionLabel` / `optionValue`, sans mapping manuel.
|
|
4
|
+
import { describe, expect, it } from "bun:test"
|
|
5
|
+
import { normalizeCellOptions } from "./spreadsheetOptions"
|
|
6
|
+
|
|
7
|
+
const users = [
|
|
8
|
+
{ _id: "u_8f3a", name: "Ada Lovelace", value: "ADA" },
|
|
9
|
+
{ _id: "u_1b2c", name: "Alan Turing", value: "ALAN" },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
describe("normalizeCellOptions — sans accesseurs (forme historique)", () => {
|
|
13
|
+
it("laisse passer `{ value, label, color? }`", () => {
|
|
14
|
+
const opts = [{ value: "tea", label: "Tea", color: "#ede9fe" }]
|
|
15
|
+
expect(normalizeCellOptions(opts)).toEqual([{ value: "tea", label: "Tea", color: "#ede9fe" }])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("accepte un label numérique et l'absence de couleur", () => {
|
|
19
|
+
expect(normalizeCellOptions([{ value: 1, label: 1 }])).toEqual([{ value: 1, label: 1 }])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("normalise une option primitive", () => {
|
|
23
|
+
expect(normalizeCellOptions(["a", 3])).toEqual([
|
|
24
|
+
{ value: "a", label: "a" },
|
|
25
|
+
{ value: 3, label: "3" },
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("replie le label sur la valeur string si `label` manque", () => {
|
|
30
|
+
expect(normalizeCellOptions([{ value: "it" }])).toEqual([{ value: "it", label: "it" }])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("tolère une entrée qui n'est pas un tableau", () => {
|
|
34
|
+
expect(normalizeCellOptions(false)).toEqual([])
|
|
35
|
+
expect(normalizeCellOptions(undefined)).toEqual([])
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe("normalizeCellOptions — `optionLabel` / `optionValue`", () => {
|
|
40
|
+
it("mappe `[{ _id, name }]` par nom de clé", () => {
|
|
41
|
+
expect(normalizeCellOptions(users, "name", "_id")).toEqual([
|
|
42
|
+
{ value: "u_8f3a", label: "Ada Lovelace" },
|
|
43
|
+
{ value: "u_1b2c", label: "Alan Turing" },
|
|
44
|
+
])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("mappe par fonction, comme `option-label` de QSelect", () => {
|
|
48
|
+
expect(normalizeCellOptions(users, (u) => u.name, (u) => u._id)).toEqual([
|
|
49
|
+
{ value: "u_8f3a", label: "Ada Lovelace" },
|
|
50
|
+
{ value: "u_1b2c", label: "Alan Turing" },
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("permet de stocker le code métier (`value`) plutôt que l'`_id`", () => {
|
|
55
|
+
expect(normalizeCellOptions(users, "name", "value")[0]).toEqual({
|
|
56
|
+
value: "ADA",
|
|
57
|
+
label: "Ada Lovelace",
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("transmet `color` tel quel (décoré côté app)", () => {
|
|
62
|
+
const decorated = users.map((u) => ({ ...u, color: u.value === "ADA" ? "#ede9fe" : "#dcfce7" }))
|
|
63
|
+
const opts = normalizeCellOptions(decorated, "name", "_id")
|
|
64
|
+
expect(opts[0]).toEqual({ value: "u_8f3a", label: "Ada Lovelace", color: "#ede9fe" })
|
|
65
|
+
expect(opts[1]!.color).toBe("#dcfce7")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("replie le label sur la valeur si la clé de label est absente de l'objet", () => {
|
|
69
|
+
expect(normalizeCellOptions([{ _id: "x" }], "name", "_id")).toEqual([{ value: "x", label: "x" }])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("ne mute pas les objets source", () => {
|
|
73
|
+
const source = [{ _id: "u_1", name: "Ada" }]
|
|
74
|
+
normalizeCellOptions(source, "name", "_id")
|
|
75
|
+
expect(source).toEqual([{ _id: "u_1", name: "Ada" }])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("produit une nouvelle liste à chaque appel (le cache est géré par le composant)", () => {
|
|
79
|
+
const a = normalizeCellOptions(users, "name", "_id")
|
|
80
|
+
const b = normalizeCellOptions(users, "name", "_id")
|
|
81
|
+
expect(a).not.toBe(b)
|
|
82
|
+
expect(a).toEqual(b)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Options de cellule de QSpreadsheet (`<q-spreadsheet>`, colonnes `select` / `multiselect`).
|
|
2
|
+
//
|
|
3
|
+
// Une colonne reçoit `options` — par défaut sous la forme `{ value, label, color? }`. Avec
|
|
4
|
+
// `optionLabel` / `optionValue`, **n'importe quelle forme d'objet** est acceptée (ex. le
|
|
5
|
+
// `[{ _id, name }]` renvoyé par une API) : on la **normalise une seule fois** vers la forme
|
|
6
|
+
// interne. C'est le point clé — badge, info-bulle, éditeur, tri, filtre et copie/CSV lisent
|
|
7
|
+
// tous la même forme, donc aucun risque d'incohérence entre deux sites.
|
|
8
|
+
//
|
|
9
|
+
// Pur (aucun DOM) → testable hors navigateur, comme `spreadsheetValidation.ts`.
|
|
10
|
+
|
|
11
|
+
import { optionsOf } from "./select"
|
|
12
|
+
|
|
13
|
+
/** Option interne normalisée : la **seule** forme lue par le composant. */
|
|
14
|
+
export interface QSpreadsheetCellOption {
|
|
15
|
+
/**
|
|
16
|
+
* Valeur stockée dans la cellule. Doit être **primitive** (string / number / boolean),
|
|
17
|
+
* unique dans la liste : le composant compare les valeurs par `===` et le document est
|
|
18
|
+
* sérialisé par `toJSON()`.
|
|
19
|
+
*/
|
|
20
|
+
value: any
|
|
21
|
+
/**
|
|
22
|
+
* Libellé affiché. Les nombres sont acceptés (`{ value: 1, label: 1 }` pour une note, un
|
|
23
|
+
* niveau…) : le composant les normalise en string partout où il fait des opérations de
|
|
24
|
+
* chaîne (recherche, filtre, tri, éditeur).
|
|
25
|
+
*/
|
|
26
|
+
label: string | number
|
|
27
|
+
/** Couleur du badge (token ou hex, ex. "positive", "#22c55e") */
|
|
28
|
+
color?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Accès à un champ d'une option : nom de clé (`"name"`, `"_id"`) **ou** fonction
|
|
33
|
+
* (`(o) => o.name`), comme l'`option-label` / `option-value` de `<q-select>`.
|
|
34
|
+
*/
|
|
35
|
+
export type QSpreadsheetOptionAccessor = string | ((opt: any) => any)
|
|
36
|
+
|
|
37
|
+
const read = (opt: any, accessor: QSpreadsheetOptionAccessor | undefined): any =>
|
|
38
|
+
typeof accessor === "function" ? accessor(opt) : accessor === undefined ? undefined : opt?.[accessor]
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalise les options d'une colonne vers `{ value, label, color? }`.
|
|
42
|
+
*
|
|
43
|
+
* - `optionLabel` / `optionValue` : clé ou fonction — défauts `"label"` / `"value"` (donc une
|
|
44
|
+
* colonne déjà écrite sous la forme `{ value, label }` n'est pas modifiée) ;
|
|
45
|
+
* - une option **primitive** (`"a"`, `3`) devient `{ value, label: String(value) }` ;
|
|
46
|
+
* - `label` absent → repli sur `String(value)` ;
|
|
47
|
+
* - `color` est **transmis tel quel** (la couleur est une décision de présentation) ;
|
|
48
|
+
* - `options` qui n'est pas un tableau → `[]` (jamais d'exception).
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeCellOptions(
|
|
51
|
+
options: unknown,
|
|
52
|
+
optionLabel?: QSpreadsheetOptionAccessor,
|
|
53
|
+
optionValue?: QSpreadsheetOptionAccessor,
|
|
54
|
+
): QSpreadsheetCellOption[] {
|
|
55
|
+
return optionsOf(options).map((opt): QSpreadsheetCellOption => {
|
|
56
|
+
if (opt === null || typeof opt !== "object") {
|
|
57
|
+
return { value: opt, label: String(opt ?? "") }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const value = read(opt, optionValue ?? "value")
|
|
61
|
+
const label = read(opt, optionLabel ?? "label")
|
|
62
|
+
|
|
63
|
+
const out: QSpreadsheetCellOption = {
|
|
64
|
+
value,
|
|
65
|
+
label: label === undefined || label === null ? String(value ?? "") : label,
|
|
66
|
+
}
|
|
67
|
+
if (opt.color !== undefined) out.color = opt.color
|
|
68
|
+
return out
|
|
69
|
+
})
|
|
70
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Tests unitaires de la validation de colonne de QSpreadsheet (`lib/spreadsheetValidation.ts`).
|
|
2
|
+
// L'accès à ArkType est **injecté** : on teste l'ordonnancement des règles sans bundler
|
|
3
|
+
// ni navigateur, et on vérifie qu'une expression est bien transmise telle quelle.
|
|
4
|
+
import { describe, expect, it } from "bun:test"
|
|
5
|
+
import { checkValidation, normalizeValidation, type ArkCheck } from "./spreadsheetValidation"
|
|
6
|
+
|
|
7
|
+
/** Faux ArkType : échoue pour toute valeur >= 4 (comme une règle `"number < 4"`). */
|
|
8
|
+
const fakeArk: ArkCheck = async (schema, value) =>
|
|
9
|
+
schema === "number < 4" && typeof value === "number" && value >= 4 ? "must be < 4" : null
|
|
10
|
+
|
|
11
|
+
const noArk: ArkCheck = async () => {
|
|
12
|
+
throw new Error("arkCheck ne devait pas être appelé")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("normalizeValidation", () => {
|
|
16
|
+
it("une chaîne devient un schéma ArkType", () => {
|
|
17
|
+
expect(normalizeValidation("number < 4")).toEqual({ schema: "number < 4" })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("un objet est laissé tel quel", () => {
|
|
21
|
+
const rule = { min: 0, max: 10 }
|
|
22
|
+
expect(normalizeValidation(rule)).toBe(rule)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("undefined → undefined", () => {
|
|
26
|
+
expect(normalizeValidation(undefined)).toBeUndefined()
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe("checkValidation — expression directe (« number < 4 »)", () => {
|
|
31
|
+
it("transmet l'expression à ArkType et remonte son message", async () => {
|
|
32
|
+
expect(await checkValidation("number < 4", 3, fakeArk)).toBeNull()
|
|
33
|
+
expect(await checkValidation("number < 4", 4, fakeArk)).toBe("must be < 4")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("`message` surcharge le message d'ArkType", async () => {
|
|
37
|
+
const rule = { schema: "number < 4", message: "Trop grand" }
|
|
38
|
+
expect(await checkValidation(rule, 9, fakeArk)).toBe("Trop grand")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("une valeur vide passe (comme la forme objet)", async () => {
|
|
42
|
+
expect(await checkValidation("number < 4", null, noArk)).toBeNull()
|
|
43
|
+
expect(await checkValidation("number < 4", "", noArk)).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("`required` refuse le vide **avant** d'appeler ArkType", async () => {
|
|
47
|
+
expect(await checkValidation({ required: true, schema: "number < 4" }, null, noArk)).toBe(
|
|
48
|
+
"Required",
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe("checkValidation — règle objet", () => {
|
|
54
|
+
it("sans règle → null", async () => {
|
|
55
|
+
expect(await checkValidation(undefined, 42, noArk)).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("required + message", async () => {
|
|
59
|
+
expect(await checkValidation({ required: true }, "", noArk)).toBe("Required")
|
|
60
|
+
expect(await checkValidation({ required: true, message: "Obligatoire" }, [], noArk)).toBe(
|
|
61
|
+
"Obligatoire",
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("list : une valeur (mode simple)", async () => {
|
|
66
|
+
const rule = { list: ["a", "b"] }
|
|
67
|
+
expect(await checkValidation(rule, "a", noArk)).toBeNull()
|
|
68
|
+
expect(await checkValidation(rule, "z", noArk)).toBe("Not in the allowed list")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("list : chaque valeur (multiselect)", async () => {
|
|
72
|
+
const rule = { list: [1, 2, 3] }
|
|
73
|
+
expect(await checkValidation(rule, [1, 3], noArk)).toBeNull()
|
|
74
|
+
expect(await checkValidation(rule, [1, 9], noArk)).toBe("Not in the allowed list")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("list : compare en texte (1 ≡ « 1 »)", async () => {
|
|
78
|
+
expect(await checkValidation({ list: ["1", "2"] }, 1, noArk)).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("integer / min / max", async () => {
|
|
82
|
+
expect(await checkValidation({ integer: true }, 2.5, noArk)).toBe("Integer required")
|
|
83
|
+
expect(await checkValidation({ min: 0 }, -1, noArk)).toBe("Value below minimum (0)")
|
|
84
|
+
expect(await checkValidation({ max: 100 }, 150, noArk)).toBe("Value above maximum (100)")
|
|
85
|
+
expect(await checkValidation({ min: 0, max: 100 }, 42, noArk)).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("min/max ne s'appliquent qu'aux nombres", async () => {
|
|
89
|
+
expect(await checkValidation({ min: 0 }, "abc", noArk)).toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("pattern (texte seulement)", async () => {
|
|
93
|
+
const rule = { pattern: "^\\d{4}$" }
|
|
94
|
+
expect(await checkValidation(rule, "2026", noArk)).toBeNull()
|
|
95
|
+
expect(await checkValidation(rule, "26", noArk)).toBe(
|
|
96
|
+
"Value does not match the required format",
|
|
97
|
+
)
|
|
98
|
+
expect(await checkValidation(rule, 2026 as any, noArk)).toBeNull() // non-string : ignoré
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("le schéma est évalué en dernier (min échoue avant)", async () => {
|
|
102
|
+
const rule = { min: 0, schema: "number < 4" }
|
|
103
|
+
expect(await checkValidation(rule, -1, fakeArk)).toBe("Value below minimum (0)")
|
|
104
|
+
expect(await checkValidation(rule, 9, fakeArk)).toBe("must be < 4")
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Validation de colonne de QSpreadsheet (`<q-spreadsheet>`).
|
|
2
|
+
//
|
|
3
|
+
// Vit dans `lib/` — et non dans le SFC — pour être **testable sans navigateur** : c'est
|
|
4
|
+
// exactement le code du composant, seul l'accès à ArkType est **injecté** (`ArkCheck`).
|
|
5
|
+
//
|
|
6
|
+
// Deux écritures possibles pour une colonne (ou une plage) :
|
|
7
|
+
//
|
|
8
|
+
// { name: "score", validation: { min: 0, max: 100, integer: true } } // objet
|
|
9
|
+
// { name: "priority", validation: "number < 4" } // expression ArkType
|
|
10
|
+
//
|
|
11
|
+
// Une **chaîne** est une expression ArkType : c'est le raccourci de `{ schema: "…" }`.
|
|
12
|
+
// Les deux formes se combinent (`{ required: true, schema: "number < 4" }`) et `message`
|
|
13
|
+
// surcharge le message de la règle qui échoue.
|
|
14
|
+
|
|
15
|
+
/** Règles de validation d'une colonne (ou d'une plage de cellules). */
|
|
16
|
+
export interface QSpreadsheetValidation {
|
|
17
|
+
/** Valeur minimale (types numériques / entiers) */
|
|
18
|
+
min?: number
|
|
19
|
+
/** Valeur maximale (types numériques / entiers) */
|
|
20
|
+
max?: number
|
|
21
|
+
/** Exige un entier (si présent, valide les entiers) */
|
|
22
|
+
integer?: boolean
|
|
23
|
+
/** Expression régulière validée contre le texte */
|
|
24
|
+
pattern?: string
|
|
25
|
+
/** Champ obligatoire (vide refusé) */
|
|
26
|
+
required?: boolean
|
|
27
|
+
/** Valeurs autorisées (liste) */
|
|
28
|
+
list?: any[]
|
|
29
|
+
/** Message d'erreur affiché (sinon message générique) */
|
|
30
|
+
message?: string
|
|
31
|
+
/**
|
|
32
|
+
* Expression **ArkType** — ex. `"number < 4"`,
|
|
33
|
+
* `"number.integer & number >= 0 & number <= 100"`, `"string.email"`.
|
|
34
|
+
* Équivalent d'une règle écrite directement en chaîne.
|
|
35
|
+
*/
|
|
36
|
+
schema?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Règle de validation : objet `{ min, max, … }` **ou** expression ArkType écrite
|
|
41
|
+
* directement (`validation: "number < 4"`).
|
|
42
|
+
*/
|
|
43
|
+
export type QSpreadsheetValidationRule = QSpreadsheetValidation | string
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Vérification d'un schéma ArkType, **injectée** par le composant (import dynamique +
|
|
47
|
+
* mémoïsation + `try/catch`) : renvoie un message d'erreur, ou `null` si la valeur passe.
|
|
48
|
+
*/
|
|
49
|
+
export type ArkCheck = (schema: string, value: unknown) => Promise<string | null>
|
|
50
|
+
|
|
51
|
+
/** Normalise une règle : une chaîne devient un schéma ArkType (`{ schema }`). */
|
|
52
|
+
export const normalizeValidation = (
|
|
53
|
+
rule: QSpreadsheetValidationRule | undefined,
|
|
54
|
+
): QSpreadsheetValidation | undefined => (typeof rule === "string" ? { schema: rule } : rule)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Valide une valeur contre une règle. Renvoie le message d'erreur, ou `null`.
|
|
58
|
+
*
|
|
59
|
+
* Ordre : `required` → vide toléré → `list` → `integer` → `min` / `max` → `pattern` →
|
|
60
|
+
* `schema` (ArkType). Une valeur **vide** passe toutes les règles sauf `required`.
|
|
61
|
+
* En `multiselect` (valeur = tableau), `list` s'applique **à chaque valeur** et un tableau
|
|
62
|
+
* vide compte comme vide, donc `required` le refuse.
|
|
63
|
+
*/
|
|
64
|
+
export async function checkValidation(
|
|
65
|
+
rule: QSpreadsheetValidationRule | undefined,
|
|
66
|
+
value: any,
|
|
67
|
+
arkCheck: ArkCheck,
|
|
68
|
+
): Promise<string | null> {
|
|
69
|
+
const v = normalizeValidation(rule)
|
|
70
|
+
if (!v) return null
|
|
71
|
+
|
|
72
|
+
const multi = Array.isArray(value)
|
|
73
|
+
const blank =
|
|
74
|
+
value === null || value === undefined || value === "" || (multi && value.length === 0)
|
|
75
|
+
if (v.required && blank) return v.message ?? "Required"
|
|
76
|
+
if (blank) return null // champ vide autorisé (sauf required)
|
|
77
|
+
|
|
78
|
+
if (v.list) {
|
|
79
|
+
// Choix multiples : chacune des valeurs doit être dans la liste
|
|
80
|
+
const values = multi ? value : [value]
|
|
81
|
+
if (values.some((x) => !v.list!.some((y) => String(y) === String(x))))
|
|
82
|
+
return v.message ?? "Not in the allowed list"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (v.integer && typeof value === "number" && !Number.isInteger(value))
|
|
86
|
+
return v.message ?? "Integer required"
|
|
87
|
+
|
|
88
|
+
if (typeof value === "number") {
|
|
89
|
+
if (v.min !== undefined && value < v.min) return v.message ?? `Value below minimum (${v.min})`
|
|
90
|
+
if (v.max !== undefined && value > v.max) return v.message ?? `Value above maximum (${v.max})`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (v.pattern && typeof value === "string" && !new RegExp(v.pattern).test(value))
|
|
94
|
+
return v.message ?? "Value does not match the required format"
|
|
95
|
+
|
|
96
|
+
if (v.schema) {
|
|
97
|
+
const e = await arkCheck(v.schema, value)
|
|
98
|
+
if (e) return v.message ?? e
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null
|
|
102
|
+
}
|