@dnax/ui 0.0.55 → 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.
|
@@ -8,11 +8,19 @@
|
|
|
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"
|
|
11
15
|
import type {
|
|
12
16
|
QSpreadsheetValidation,
|
|
13
17
|
QSpreadsheetValidationRule,
|
|
14
18
|
} from "../lib/spreadsheetValidation"
|
|
15
19
|
|
|
20
|
+
export type {
|
|
21
|
+
QSpreadsheetCellOption,
|
|
22
|
+
QSpreadsheetOptionAccessor,
|
|
23
|
+
} from "../lib/spreadsheetOptions"
|
|
16
24
|
export type {
|
|
17
25
|
QSpreadsheetValidation,
|
|
18
26
|
QSpreadsheetValidationRule,
|
|
@@ -31,18 +39,9 @@ export type QSpreadsheetCellType =
|
|
|
31
39
|
| "select"
|
|
32
40
|
| "multiselect"
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Libellé affiché. Les nombres sont acceptés (`{ value: 1, label: 1 }` pour une
|
|
39
|
-
* note, un niveau…) : le composant les normalise en string partout où il fait des
|
|
40
|
-
* opérations de chaîne (recherche, filtre, tri, éditeur).
|
|
41
|
-
*/
|
|
42
|
-
label: string | number
|
|
43
|
-
/** Couleur du badge (token ou hex, ex. "positive", "#22c55e") */
|
|
44
|
-
color?: string
|
|
45
|
-
}
|
|
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.
|
|
46
45
|
|
|
47
46
|
export interface QSpreadsheetColumn {
|
|
48
47
|
/** Clé de la propriété dans la ligne */
|
|
@@ -58,8 +57,23 @@ export interface QSpreadsheetColumn {
|
|
|
58
57
|
type?: QSpreadsheetCellType
|
|
59
58
|
/** La cellule est éditable (Entrée / double-clic / frappe) */
|
|
60
59
|
editable?: boolean
|
|
61
|
-
/**
|
|
62
|
-
|
|
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
|
|
63
77
|
/** Pour type "select" / "multiselect" : rend les options actives en badge coloré (sinon texte) */
|
|
64
78
|
chip?: boolean
|
|
65
79
|
/** Validation à la saisie : objet `{ min, max, … }` ou expression ArkType (`"number < 4"`) */
|
|
@@ -157,6 +171,7 @@ import {
|
|
|
157
171
|
parseCsv,
|
|
158
172
|
} from "../lib/spreadsheet"
|
|
159
173
|
import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
|
|
174
|
+
import { normalizeCellOptions } from "../lib/spreadsheetOptions"
|
|
160
175
|
import {
|
|
161
176
|
DEFAULT_ROW_KEY,
|
|
162
177
|
diffDocuments,
|
|
@@ -513,6 +528,38 @@ const colNameAt = (i: number) => cols.value[i]?.name ?? ""
|
|
|
513
528
|
const colOf = (name: string) => cols.value.find((c) => c.name === name)
|
|
514
529
|
|
|
515
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
|
+
|
|
516
563
|
/**
|
|
517
564
|
* Valeurs d'une cellule à choix multiples, toujours sous forme de liste : un tableau est
|
|
518
565
|
* repris tel quel, une valeur nue (`null`, `"it"`…) devient `[valeur]` (ou `[]` si vide).
|
|
@@ -526,7 +573,7 @@ const multiValues = (raw: any): any[] => {
|
|
|
526
573
|
/** Libellés d'une cellule à choix multiples (le `value` brut si l'option est inconnue) */
|
|
527
574
|
const multiLabels = (col: QSpreadsheetColumn | undefined, raw: any): string[] =>
|
|
528
575
|
multiValues(raw).map((v) => {
|
|
529
|
-
const opt = col
|
|
576
|
+
const opt = cellOptionsOf(col).find((o) => o.value === v)
|
|
530
577
|
return String(opt?.label ?? v)
|
|
531
578
|
})
|
|
532
579
|
|
|
@@ -538,7 +585,7 @@ const choiceText = (col: QSpreadsheetColumn | undefined, raw: any): string => {
|
|
|
538
585
|
if (col?.type === "multiselect") return multiLabels(col, raw).join(", ")
|
|
539
586
|
if (isBlankValue(raw)) return ""
|
|
540
587
|
if (col?.type === "select") {
|
|
541
|
-
const opt = col.
|
|
588
|
+
const opt = cellOptionsOf(col).find((o) => o.value === raw)
|
|
542
589
|
if (opt?.label !== undefined) return String(opt.label)
|
|
543
590
|
}
|
|
544
591
|
return String(raw)
|
|
@@ -838,7 +885,7 @@ const startEdit = (row: number, column: string, initial?: string) => {
|
|
|
838
885
|
if (col?.editable === false || col?.type === "boolean") return
|
|
839
886
|
const raw = state.value[row]?.[column]
|
|
840
887
|
if (col?.type === "select") {
|
|
841
|
-
const opt = col.
|
|
888
|
+
const opt = cellOptionsOf(col).find((o) => o.value === raw)
|
|
842
889
|
// `label` peut être un nombre (options numériques) : `draft` alimente un
|
|
843
890
|
// <input>/<textarea> et sert à `.trim()` / `.toLowerCase()` → toujours une string.
|
|
844
891
|
draft.value = String(initial ?? opt?.label ?? "")
|
|
@@ -925,7 +972,9 @@ const coerceValue = (col: QSpreadsheetColumn | undefined, text: unknown, old: an
|
|
|
925
972
|
return raw === "true" ? true : raw === "false" ? false : old
|
|
926
973
|
}
|
|
927
974
|
if (col?.type === "select") {
|
|
928
|
-
const opt = col.
|
|
975
|
+
const opt = cellOptionsOf(col).find(
|
|
976
|
+
(o) => String(o.label ?? "") === raw || String(o.value) === raw,
|
|
977
|
+
)
|
|
929
978
|
return opt ? opt.value : old
|
|
930
979
|
}
|
|
931
980
|
// `multiselect` : collage / saisie « a, b » → tableau de valeurs d'option
|
|
@@ -936,7 +985,7 @@ const coerceValue = (col: QSpreadsheetColumn | undefined, text: unknown, old: an
|
|
|
936
985
|
.filter((s) => s !== "")
|
|
937
986
|
if (!parts.length) return null
|
|
938
987
|
return parts.map((part) => {
|
|
939
|
-
const opt = col.
|
|
988
|
+
const opt = cellOptionsOf(col).find(
|
|
940
989
|
(o) => String(o.label ?? "") === part || String(o.value) === part,
|
|
941
990
|
)
|
|
942
991
|
return opt ? opt.value : part
|
|
@@ -1637,7 +1686,7 @@ const selectOptions = computed<QSpreadsheetCellOption[]>(() => {
|
|
|
1637
1686
|
const col = colOf(editing.value.column)
|
|
1638
1687
|
if (col?.type !== "select" && col?.type !== "multiselect") return []
|
|
1639
1688
|
const q = String(draft.value ?? "").trim().toLowerCase()
|
|
1640
|
-
return (col
|
|
1689
|
+
return cellOptionsOf(col).filter(
|
|
1641
1690
|
(o) => !q || String(o.label ?? o.value ?? "").toLowerCase().includes(q),
|
|
1642
1691
|
)
|
|
1643
1692
|
})
|
|
@@ -2495,7 +2544,7 @@ const badgeStyle = (opt: QSpreadsheetCellOption) => {
|
|
|
2495
2544
|
|
|
2496
2545
|
const badgeOf = (col: QSpreadsheetColumn | undefined, val: any) => {
|
|
2497
2546
|
if (col?.type !== "select" || !col.chip) return undefined
|
|
2498
|
-
const opt = col.
|
|
2547
|
+
const opt = cellOptionsOf(col).find((o) => o.value === val)
|
|
2499
2548
|
if (!opt) return undefined
|
|
2500
2549
|
return { label: opt.label, style: badgeStyle(opt) }
|
|
2501
2550
|
}
|
|
@@ -2504,7 +2553,7 @@ const badgeOf = (col: QSpreadsheetColumn | undefined, val: any) => {
|
|
|
2504
2553
|
const multiBadges = (col: QSpreadsheetColumn | undefined, val: any) => {
|
|
2505
2554
|
if (col?.type !== "multiselect" || !col.chip) return []
|
|
2506
2555
|
return multiValues(val).map((v) => {
|
|
2507
|
-
const opt = col.
|
|
2556
|
+
const opt = cellOptionsOf(col).find((o) => o.value === v)
|
|
2508
2557
|
return {
|
|
2509
2558
|
label: String(opt?.label ?? v),
|
|
2510
2559
|
style: badgeStyle(opt ?? { value: v, label: String(v) }),
|
|
@@ -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
|
+
}
|