@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.
@@ -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
- * Comble les cellules **vides** d'après `prefill` (sans jamais écraser une valeur
1283
- * existante) et renvoie le nombre de cellules remplies. Utile après avoir chargé
1284
- * `rows` : les données déjà présentes ne sont pas touchées à l'initialisation.
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
- const applyPrefill = (): number => {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.58",
3
+ "version": "0.0.59",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",