@dnax/ui 0.0.58 → 0.0.59
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/QSpreadsheet.vue +77 -12
- package/lib/spreadsheetRows.test.ts +53 -0
- package/lib/spreadsheetRows.ts +34 -0
- package/package.json +1 -1
|
@@ -193,6 +193,7 @@ import {
|
|
|
193
193
|
import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
|
|
194
194
|
import { normalizeCellOptions } from "../lib/spreadsheetOptions"
|
|
195
195
|
import { inLockRect, lockRects, prefillForRow } from "../lib/spreadsheetZones"
|
|
196
|
+
import { canAddRows, canRemoveRows, clampRows } from "../lib/spreadsheetRows"
|
|
196
197
|
import {
|
|
197
198
|
DEFAULT_ROW_KEY,
|
|
198
199
|
diffDocuments,
|
|
@@ -264,6 +265,27 @@ interface Props {
|
|
|
264
265
|
* les noms de colonnes inconnus sont ignorés.
|
|
265
266
|
*/
|
|
266
267
|
prefill?: QSpreadsheetFill[]
|
|
268
|
+
/**
|
|
269
|
+
* Applique `prefill` **automatiquement** aux cellules **vides** dès que le document est prêt
|
|
270
|
+
* (chargement, remplacement de `rows` / `columns`) — sans avoir à appeler `applyPrefill()`.
|
|
271
|
+
* Le résultat est **écrit dans `rows`** (`update:rows`) : utiliser `v-model:rows` (ou écouter
|
|
272
|
+
* `@update:rows`) pour le recevoir. Sans historique ni marquage « modifié ». Une ligne
|
|
273
|
+
* **neuve** naît de toute façon préremplie. `false` → application sur appel explicite
|
|
274
|
+
* uniquement. (Défaut : `true`.)
|
|
275
|
+
*/
|
|
276
|
+
autoPrefill?: boolean
|
|
277
|
+
/**
|
|
278
|
+
* Nombre **minimum** de lignes : impossible de descendre en dessous (la suppression des lignes
|
|
279
|
+
* sélectionnées est refusée si elle passerait sous le plancher). Combiné avec `maxRows` à la
|
|
280
|
+
* **même** valeur, le nombre de lignes est **figé**. (Défaut : aucune limite.)
|
|
281
|
+
*/
|
|
282
|
+
minRows?: number
|
|
283
|
+
/**
|
|
284
|
+
* Nombre **maximum** de lignes : le bouton « + », l'insertion au curseur et l'import CSV
|
|
285
|
+
* s'arrêtent à cette limite (`:max-rows="1"` → feuille à **une seule** ligne). Les chargements
|
|
286
|
+
* programmatiques (`loadDocument`) ne sont pas bornés. (Défaut : aucune limite.)
|
|
287
|
+
*/
|
|
288
|
+
maxRows?: number
|
|
267
289
|
/** Affiche le numéro de ligne (colonne de gauche) */
|
|
268
290
|
showRowNumbers?: boolean
|
|
269
291
|
/** Affiche l'en-tête de colonne (lettre + label) */
|
|
@@ -499,6 +521,7 @@ const emit = defineEmits<{
|
|
|
499
521
|
"cell-edit-end": [payload: { row: number; column: string; canceled: boolean }]
|
|
500
522
|
"selection-change": [value: QSpreadsheetSelection | null]
|
|
501
523
|
"structure-change": [payload: { rows: Record<string, any>[]; columns: QSpreadsheetColumn[]; reason: string }]
|
|
524
|
+
"prefill": [payload: { count: number; mode: "auto" | "manual" }]
|
|
502
525
|
"update:dirty": [value: boolean]
|
|
503
526
|
"update:changes": [value: QSpreadsheetChanges]
|
|
504
527
|
}>()
|
|
@@ -542,6 +565,8 @@ const ensureRowKeys = (rows: Record<string, any>[] | undefined) => {
|
|
|
542
565
|
}
|
|
543
566
|
|
|
544
567
|
// ─── Données internes (copie éditable des rows) ───
|
|
568
|
+
// Préremplissage : tant que `false`, toute application auto est ignorée (avant montage).
|
|
569
|
+
let prefillReady = false
|
|
545
570
|
const state = ref<Record<string, any>[]>([])
|
|
546
571
|
watch(
|
|
547
572
|
() => props.rows,
|
|
@@ -553,6 +578,8 @@ watch(
|
|
|
553
578
|
state.value = v.map((r) => ({ ...r }))
|
|
554
579
|
// Lignes **externes** (nouveau document) → nouvelle référence de comparaison
|
|
555
580
|
if (trackReady) acceptChanges()
|
|
581
|
+
// Nouveau document → applique les défauts sur les cellules vides.
|
|
582
|
+
autoPrefill()
|
|
556
583
|
}
|
|
557
584
|
},
|
|
558
585
|
{ immediate: true },
|
|
@@ -571,6 +598,8 @@ watch(
|
|
|
571
598
|
cols.value = v.map((c) => ({ ...c }))
|
|
572
599
|
// Colonnes **externes** (nouveau schéma) → nouvelle référence de comparaison
|
|
573
600
|
if (trackReady) acceptChanges()
|
|
601
|
+
// Les zones de `prefill` dépendent des noms de colonnes → (re)applique.
|
|
602
|
+
autoPrefill()
|
|
574
603
|
}
|
|
575
604
|
},
|
|
576
605
|
{ immediate: true },
|
|
@@ -580,6 +609,12 @@ const pushCols = (next: QSpreadsheetColumn[]) => {
|
|
|
580
609
|
emit("update:columns", next)
|
|
581
610
|
}
|
|
582
611
|
|
|
612
|
+
// ─── Limites de lignes (`minRows` / `maxRows`) ───
|
|
613
|
+
/** Peut-on encore ajouter une ligne ? (bouton « + », insertion au curseur) */
|
|
614
|
+
const canAddRow = computed(() => canAddRows(state.value.length, 1, props.maxRows))
|
|
615
|
+
/** Peut-on retirer au moins une ligne ? (plancher `minRows`) */
|
|
616
|
+
const canRemoveRow = computed(() => canRemoveRows(state.value.length, 1, props.minRows))
|
|
617
|
+
|
|
583
618
|
const colIndex = (name: string) => cols.value.findIndex((c) => c.name === name)
|
|
584
619
|
const colNameAt = (i: number) => cols.value[i]?.name ?? ""
|
|
585
620
|
const colOf = (name: string) => cols.value.find((c) => c.name === name)
|
|
@@ -1279,11 +1314,11 @@ const blankRow = (at = state.value.length): Record<string, any> => {
|
|
|
1279
1314
|
}
|
|
1280
1315
|
|
|
1281
1316
|
/**
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
* `rows`
|
|
1317
|
+
* Cœur du préremplissage : ne remplit que les cellules **vides** et renvoie le nombre rempli.
|
|
1318
|
+
* `mode` distingue l'application **automatique** (silencieuse) de l'appel **explicite**
|
|
1319
|
+
* (avec historique). Émet `update:rows` (si quelque chose a changé) puis `prefill`.
|
|
1285
1320
|
*/
|
|
1286
|
-
|
|
1321
|
+
function runPrefill(mode: "auto" | "manual"): number {
|
|
1287
1322
|
const fills = props.prefill
|
|
1288
1323
|
if (!fills?.length || props.disable) return 0
|
|
1289
1324
|
const names = cols.value.map((c) => c.name)
|
|
@@ -1300,14 +1335,35 @@ const applyPrefill = (): number => {
|
|
|
1300
1335
|
return out
|
|
1301
1336
|
})
|
|
1302
1337
|
if (!count) return 0
|
|
1303
|
-
pushHistory()
|
|
1338
|
+
if (mode === "manual") pushHistory()
|
|
1304
1339
|
state.value = next
|
|
1305
1340
|
emit("update:rows", next)
|
|
1341
|
+
emit("prefill", { count, mode })
|
|
1306
1342
|
return count
|
|
1307
1343
|
}
|
|
1308
1344
|
|
|
1345
|
+
/**
|
|
1346
|
+
* Comble les cellules **vides** d'après `prefill` (jamais une valeur existante) et renvoie le
|
|
1347
|
+
* nombre de cellules remplies. Appel **explicite** (avec historique) — l'application au
|
|
1348
|
+
* chargement est automatique (cf. `autoPrefill`).
|
|
1349
|
+
*/
|
|
1350
|
+
function applyPrefill(): number {
|
|
1351
|
+
return runPrefill("manual")
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Application **automatique** (document prêt, nouveau document) : silencieuse — sans
|
|
1356
|
+
* historique, pour ne pas polluer l'annulation ni marquer la grille « modifiée ». **Écrit le
|
|
1357
|
+
* résultat dans `rows`** (`update:rows`). Désactivable via `:auto-prefill="false"`.
|
|
1358
|
+
*/
|
|
1359
|
+
function autoPrefill(): void {
|
|
1360
|
+
if (!prefillReady || props.autoPrefill === false) return
|
|
1361
|
+
runPrefill("auto")
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1309
1364
|
const addRow = (at?: number) => {
|
|
1310
1365
|
if (props.readonly || props.disable) return
|
|
1366
|
+
if (!canAddRow.value) return
|
|
1311
1367
|
pushHistory()
|
|
1312
1368
|
const idx = at ?? state.value.length
|
|
1313
1369
|
const next = [...state.value]
|
|
@@ -1320,12 +1376,14 @@ const addRow = (at?: number) => {
|
|
|
1320
1376
|
|
|
1321
1377
|
const removeSelectedRows = () => {
|
|
1322
1378
|
if (props.readonly || props.disable) return
|
|
1323
|
-
pushHistory()
|
|
1324
|
-
purgeMergesAndRules()
|
|
1325
1379
|
const rect = selRect.value
|
|
1326
1380
|
if (!rect || !state.value.length) return
|
|
1327
1381
|
const indexes = new Set<number>()
|
|
1328
1382
|
for (let r = rect.r0; r <= rect.r1; r++) indexes.add(r)
|
|
1383
|
+
// Plancher `minRows` : on refuse si la suppression descendrait en dessous.
|
|
1384
|
+
if (!canRemoveRows(state.value.length, indexes.size, props.minRows)) return
|
|
1385
|
+
pushHistory()
|
|
1386
|
+
purgeMergesAndRules()
|
|
1329
1387
|
const sorted = [...indexes].sort((a, b) => b - a)
|
|
1330
1388
|
const next = [...state.value]
|
|
1331
1389
|
for (const i of sorted) next.splice(i, 1)
|
|
@@ -2591,6 +2649,7 @@ const doSortBy = (desc = false) => {
|
|
|
2591
2649
|
// ─── Insertion au curseur (lignes / colonnes) ───
|
|
2592
2650
|
const insertRowAt = (pos: "above" | "below") => {
|
|
2593
2651
|
if (!canEdit.value) return
|
|
2652
|
+
if (!canAddRow.value) return
|
|
2594
2653
|
pushHistory()
|
|
2595
2654
|
purgeMergesAndRules()
|
|
2596
2655
|
if (!state.value.length) {
|
|
@@ -2986,6 +3045,10 @@ const onFilterSearch = (e: Event) => {
|
|
|
2986
3045
|
}
|
|
2987
3046
|
|
|
2988
3047
|
onMounted(() => {
|
|
3048
|
+
// Préremplissage automatique en premier : la référence initiale ci-dessous doit
|
|
3049
|
+
// l'inclure, sinon les défauts compteraient comme des « modifications ».
|
|
3050
|
+
prefillReady = true
|
|
3051
|
+
autoPrefill()
|
|
2989
3052
|
// Référence initiale : à partir d'ici, toute différence avec elle = « modifié »
|
|
2990
3053
|
trackReady = true
|
|
2991
3054
|
acceptChanges()
|
|
@@ -3622,6 +3685,8 @@ const importCsv = (text: string, opts: { delimiter?: string; headers?: boolean }
|
|
|
3622
3685
|
}))
|
|
3623
3686
|
data = rows
|
|
3624
3687
|
}
|
|
3688
|
+
// Plafond `maxRows` : l'import ne crée pas plus de lignes que la limite.
|
|
3689
|
+
data = clampRows(data, props.maxRows)
|
|
3625
3690
|
const rowObjs = data.map((row) => {
|
|
3626
3691
|
const obj: Record<string, any> = { [ROW_KEY]: newRowKey() }
|
|
3627
3692
|
colsArr.forEach((c, ci) => {
|
|
@@ -4395,11 +4460,11 @@ defineExpose({
|
|
|
4395
4460
|
</span>
|
|
4396
4461
|
|
|
4397
4462
|
<span class="q-spreadsheet__tb-group">
|
|
4398
|
-
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable" title="Add row" aria-label="Add row" @click="addRow()">
|
|
4463
|
+
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable || !canAddRow" title="Add row" aria-label="Add row" @click="addRow()">
|
|
4399
4464
|
<Icon :icon="icons.rows3" aria-hidden="true" />
|
|
4400
4465
|
<Icon :icon="icons.plus" class="q-spreadsheet__tool-badge" aria-hidden="true" />
|
|
4401
4466
|
</button>
|
|
4402
|
-
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable || !selRect" title="Remove selected row(s)" aria-label="Remove rows" @click="removeSelectedRows">
|
|
4467
|
+
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable || !selRect || !canRemoveRow" title="Remove selected row(s)" aria-label="Remove rows" @click="removeSelectedRows">
|
|
4403
4468
|
<Icon :icon="icons.minus" aria-hidden="true" />
|
|
4404
4469
|
</button>
|
|
4405
4470
|
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable" title="Add column" aria-label="Add column" @click="addColumn()">
|
|
@@ -5012,13 +5077,13 @@ defineExpose({
|
|
|
5012
5077
|
</button>
|
|
5013
5078
|
|
|
5014
5079
|
<div class="q-spreadsheet__sep" />
|
|
5015
|
-
<button type="button" class="q-spreadsheet__mi" :disabled="!canEdit" @click="insertRowAt('above')">
|
|
5080
|
+
<button type="button" class="q-spreadsheet__mi" :disabled="!canEdit || !canAddRow" @click="insertRowAt('above')">
|
|
5016
5081
|
<Icon :icon="icons.arrowUp" aria-hidden="true" /> {{ t('insertRowAbove') }}
|
|
5017
5082
|
</button>
|
|
5018
|
-
<button type="button" class="q-spreadsheet__mi" :disabled="!canEdit" @click="insertRowAt('below')">
|
|
5083
|
+
<button type="button" class="q-spreadsheet__mi" :disabled="!canEdit || !canAddRow" @click="insertRowAt('below')">
|
|
5019
5084
|
<Icon :icon="icons.arrowDown" aria-hidden="true" /> {{ t('insertRowBelow') }}
|
|
5020
5085
|
</button>
|
|
5021
|
-
<button type="button" class="q-spreadsheet__mi" :disabled="!canEdit || !selRect" @click="removeSelectedRows">
|
|
5086
|
+
<button type="button" class="q-spreadsheet__mi" :disabled="!canEdit || !selRect || !canRemoveRow" @click="removeSelectedRows">
|
|
5022
5087
|
<Icon :icon="icons.trash2" aria-hidden="true" /> {{ t('deleteRows') }}
|
|
5023
5088
|
</button>
|
|
5024
5089
|
<div class="q-spreadsheet__sep" />
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Tests unitaires des **limites de lignes** (`lib/spreadsheetRows.ts`) : `maxRows` / `minRows`.
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import { canAddRows, canRemoveRows, clampRows, removalsAllowed, rowRoom } from "./spreadsheetRows"
|
|
4
|
+
|
|
5
|
+
describe("rowRoom / canAddRows", () => {
|
|
6
|
+
it("sans max : illimité", () => {
|
|
7
|
+
expect(rowRoom(3, undefined)).toBe(Number.POSITIVE_INFINITY)
|
|
8
|
+
expect(canAddRows(3, 99, undefined)).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("borne par le max", () => {
|
|
12
|
+
expect(rowRoom(0, 1)).toBe(1)
|
|
13
|
+
expect(rowRoom(1, 1)).toBe(0)
|
|
14
|
+
expect(rowRoom(5, 1)).toBe(0)
|
|
15
|
+
expect(canAddRows(0, 1, 1)).toBe(true)
|
|
16
|
+
expect(canAddRows(1, 1, 1)).toBe(false)
|
|
17
|
+
expect(canAddRows(0, 2, 1)).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("ignore un max non fini (Infinity / NaN)", () => {
|
|
21
|
+
expect(rowRoom(4, Number.POSITIVE_INFINITY)).toBe(Number.POSITIVE_INFINITY)
|
|
22
|
+
expect(rowRoom(4, Number.NaN)).toBe(Number.POSITIVE_INFINITY)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe("removalsAllowed / canRemoveRows", () => {
|
|
27
|
+
it("sans min : on peut tout retirer", () => {
|
|
28
|
+
expect(removalsAllowed(3, undefined)).toBe(3)
|
|
29
|
+
expect(canRemoveRows(3, 3, undefined)).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("plancher par le min", () => {
|
|
33
|
+
expect(removalsAllowed(3, 1)).toBe(2)
|
|
34
|
+
expect(removalsAllowed(1, 1)).toBe(0)
|
|
35
|
+
expect(removalsAllowed(0, 1)).toBe(0)
|
|
36
|
+
expect(canRemoveRows(3, 2, 1)).toBe(true)
|
|
37
|
+
expect(canRemoveRows(3, 3, 1)).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("clampRows", () => {
|
|
42
|
+
it("tronque au-delà du max", () => {
|
|
43
|
+
expect(clampRows([1, 2, 3], 2)).toEqual([1, 2])
|
|
44
|
+
expect(clampRows([1], 5)).toEqual([1])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("renvoie la **même** référence si rien à couper (ou sans max)", () => {
|
|
48
|
+
const rows = [1, 2]
|
|
49
|
+
expect(clampRows(rows, 2)).toBe(rows)
|
|
50
|
+
expect(clampRows(rows, 5)).toBe(rows)
|
|
51
|
+
expect(clampRows(rows, undefined)).toBe(rows)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// **Limites du nombre de lignes** de QSpreadsheet (`<q-spreadsheet>`) : props `maxRows` /
|
|
2
|
+
// `minRows`. Elles bornent les opérations **utilisateur** (ajout / insertion / suppression de
|
|
3
|
+
// lignes, import CSV) — jamais les chargements programmatiques (`loadDocument`), comme
|
|
4
|
+
// `readonly` / `disable` bornent les éditions.
|
|
5
|
+
//
|
|
6
|
+
// Logique **pure**, testable hors navigateur — comme `spreadsheetZones.ts`.
|
|
7
|
+
|
|
8
|
+
/** Nombre de lignes qu'on peut encore **ajouter** avant `max` (`Infinity` si pas de limite). */
|
|
9
|
+
export function rowRoom(current: number, max?: number): number {
|
|
10
|
+
if (typeof max !== "number" || !Number.isFinite(max)) return Number.POSITIVE_INFINITY
|
|
11
|
+
return Math.max(0, max - current)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Peut-on ajouter `count` lignes à un document de `current` lignes ? */
|
|
15
|
+
export function canAddRows(current: number, count = 1, max?: number): boolean {
|
|
16
|
+
return count <= rowRoom(current, max)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Nombre de lignes qu'on peut **retirer** au maximum avant d'atteindre `min` (plancher 0). */
|
|
20
|
+
export function removalsAllowed(current: number, min?: number): number {
|
|
21
|
+
if (typeof min !== "number" || !Number.isFinite(min)) return Math.max(0, current)
|
|
22
|
+
return Math.max(0, current - Math.max(0, min))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Peut-on retirer `count` lignes d'un document de `current` lignes (il en reste ≥ `min`) ? */
|
|
26
|
+
export function canRemoveRows(current: number, count = 1, min?: number): boolean {
|
|
27
|
+
return count <= removalsAllowed(current, min)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Plafonne une liste de lignes à `max` (renvoie la **même** référence si rien à couper). */
|
|
31
|
+
export function clampRows<T>(rows: T[], max?: number): T[] {
|
|
32
|
+
if (typeof max !== "number" || !Number.isFinite(max) || rows.length <= max) return rows
|
|
33
|
+
return rows.slice(0, max)
|
|
34
|
+
}
|