@dnax/ui 0.0.54 → 0.0.55
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 +50 -64
- package/lib/select.ts +3 -2
- 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,16 @@
|
|
|
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
|
+
QSpreadsheetValidation,
|
|
13
|
+
QSpreadsheetValidationRule,
|
|
14
|
+
} from "../lib/spreadsheetValidation"
|
|
15
|
+
|
|
16
|
+
export type {
|
|
17
|
+
QSpreadsheetValidation,
|
|
18
|
+
QSpreadsheetValidationRule,
|
|
19
|
+
} from "../lib/spreadsheetValidation"
|
|
20
|
+
|
|
11
21
|
export type QSpreadsheetCellType =
|
|
12
22
|
| "string"
|
|
13
23
|
| "text"
|
|
@@ -52,8 +62,8 @@ export interface QSpreadsheetColumn {
|
|
|
52
62
|
options?: QSpreadsheetCellOption[]
|
|
53
63
|
/** Pour type "select" / "multiselect" : rend les options actives en badge coloré (sinon texte) */
|
|
54
64
|
chip?: boolean
|
|
55
|
-
/** Validation à la saisie : { min, max,
|
|
56
|
-
validation?:
|
|
65
|
+
/** Validation à la saisie : objet `{ min, max, … }` ou expression ArkType (`"number < 4"`) */
|
|
66
|
+
validation?: QSpreadsheetValidationRule
|
|
57
67
|
/** Formatteur de la valeur affichée */
|
|
58
68
|
format?: (val: any, row: Record<string, any>) => any
|
|
59
69
|
/** Classe CSS de la cellule (défaut : ) */
|
|
@@ -64,24 +74,9 @@ export interface QSpreadsheetColumn {
|
|
|
64
74
|
headerStyle?: string
|
|
65
75
|
}
|
|
66
76
|
|
|
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
|
-
}
|
|
77
|
+
// `QSpreadsheetValidation` et `QSpreadsheetValidationRule` vivent dans
|
|
78
|
+
// `lib/spreadsheetValidation.ts` (source unique, testée hors navigateur) et sont
|
|
79
|
+
// ré-exportées en tête de ce bloc.
|
|
85
80
|
|
|
86
81
|
/** Validation par plage de cellules (rows/cols 0-based) */
|
|
87
82
|
export interface QSpreadsheetRangeValidator {
|
|
@@ -89,7 +84,7 @@ export interface QSpreadsheetRangeValidator {
|
|
|
89
84
|
c0: number
|
|
90
85
|
r1: number
|
|
91
86
|
c1: number
|
|
92
|
-
validation:
|
|
87
|
+
validation: QSpreadsheetValidationRule
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
export interface QSpreadsheetSelection {
|
|
@@ -161,6 +156,7 @@ import {
|
|
|
161
156
|
deriveCols,
|
|
162
157
|
parseCsv,
|
|
163
158
|
} from "../lib/spreadsheet"
|
|
159
|
+
import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
|
|
164
160
|
import {
|
|
165
161
|
DEFAULT_ROW_KEY,
|
|
166
162
|
diffDocuments,
|
|
@@ -1676,7 +1672,7 @@ const toggleMultiOption = (opt: QSpreadsheetCellOption) => {
|
|
|
1676
1672
|
const next = multiSelected(opt)
|
|
1677
1673
|
? current.filter((v) => v !== opt.value)
|
|
1678
1674
|
: [...current, opt.value]
|
|
1679
|
-
validateAndSet(row, column, old, next)
|
|
1675
|
+
void validateAndSet(row, column, old, next)
|
|
1680
1676
|
}
|
|
1681
1677
|
|
|
1682
1678
|
// ─── Barre de formule (fx) ───
|
|
@@ -3854,60 +3850,50 @@ const condStyleOf = (r: number, c: number, name: string) => {
|
|
|
3854
3850
|
return Object.keys(st).length ? st : undefined
|
|
3855
3851
|
}
|
|
3856
3852
|
|
|
3857
|
-
// ════════ Validation de colonne (min / max / entier / pattern) ════════
|
|
3853
|
+
// ════════ Validation de colonne (min / max / entier / pattern / ArkType) ════════
|
|
3858
3854
|
const valErrors = ref<Record<string, string>>({})
|
|
3859
3855
|
const valKey = (r: number, name: string) => r + ":" + name
|
|
3856
|
+
|
|
3857
|
+
// ArkType : import **dynamique** (jamais dans le graphe du barrel) mémoïsé au niveau
|
|
3858
|
+
// module — une seule promesse pour toute la session, et non une par édition de cellule.
|
|
3859
|
+
let arkModule: Promise<any | null> | null = null
|
|
3860
|
+
const loadArk = (): Promise<any | null> => {
|
|
3861
|
+
arkModule ??= import("arktype")
|
|
3862
|
+
.then((m) => m as any)
|
|
3863
|
+
.catch(() => null)
|
|
3864
|
+
return arkModule
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
/**
|
|
3868
|
+
* Vérifie une expression ArkType. Une expression **invalide** (faute de syntaxe) ne doit
|
|
3869
|
+
* JAMAIS jeter : l'exception remonterait en rejet non géré et la cellule ne serait ni
|
|
3870
|
+
* validée ni commitée, sans aucun message (cf. `.memory/knowledges.md`).
|
|
3871
|
+
*/
|
|
3872
|
+
const arkCheck: ArkCheck = async (schema, value) => {
|
|
3873
|
+
const m = await loadArk()
|
|
3874
|
+
if (!m) return "ArkType unavailable"
|
|
3875
|
+
try {
|
|
3876
|
+
const problems = m.type(schema)(value)
|
|
3877
|
+
const p = Array.isArray(problems) ? problems[0] : undefined
|
|
3878
|
+
return p?.message ?? null
|
|
3879
|
+
} catch (e) {
|
|
3880
|
+
return `Invalid ArkType schema: ${(e as Error)?.message ?? String(e)}`
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
/** Validation d'une cellule : règles de la colonne, puis celles des plages qui la couvrent. */
|
|
3860
3885
|
const guardValidation = async (
|
|
3861
3886
|
row: number,
|
|
3862
3887
|
col: QSpreadsheetColumn | undefined,
|
|
3863
3888
|
value: any,
|
|
3864
3889
|
): 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)
|
|
3890
|
+
const colErr = await checkValidation(col?.validation, value, arkCheck)
|
|
3905
3891
|
if (colErr) return colErr
|
|
3906
3892
|
if (props.validators?.length && col) {
|
|
3907
3893
|
const ci = colIndex(col.name)
|
|
3908
3894
|
for (const rv of props.validators) {
|
|
3909
3895
|
if (row >= rv.r0 && row <= rv.r1 && ci >= rv.c0 && ci <= rv.c1) {
|
|
3910
|
-
const e = await
|
|
3896
|
+
const e = await checkValidation(rv.validation, value, arkCheck)
|
|
3911
3897
|
if (e) return e
|
|
3912
3898
|
}
|
|
3913
3899
|
}
|
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,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
|
+
}
|