@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.
@@ -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 = props.options.find((o) => getValue(o) === props.modelValue)
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 props.options
247
+ if (hasFilter.value) return optionList.value
242
248
  const q = query.value.trim().toLowerCase()
243
- if (!q) return props.options
244
- return props.options.filter((o) => getLabel(o).toLowerCase().includes(q))
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 = props.options.some((o) => getLabel(o).toLowerCase() === label)
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, integer?, pattern?, message? } */
56
- validation?: QSpreadsheetValidation
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
- export interface QSpreadsheetValidation {
68
- /** Valeur minimale (types numériques / entiers) */
69
- min?: number
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: QSpreadsheetValidation
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
- let ark: Promise<{ type: (s: string) => (v: unknown) => { message?: string }[] } | null> | null = null
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 check(rv.validation)
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",