@ouestfrance/sipa-bms-ui 8.48.2 → 8.49.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouestfrance/sipa-bms-ui",
3
- "version": "8.48.2",
3
+ "version": "8.49.0",
4
4
  "author": "Ouest-France BMS",
5
5
  "license": "ISC",
6
6
  "scripts": {
@@ -3,6 +3,7 @@ import BmsButton from '@/components/button/BmsButton.vue';
3
3
  import BmsIconButton from '@/components/button/BmsIconButton.vue';
4
4
  import { BmsTag } from '@/index';
5
5
  import { Save, Trash, Pencil } from 'lucide-vue-next';
6
+ import { ref } from 'vue';
6
7
 
7
8
  export default {
8
9
  title: 'Composants/table/Table',
@@ -1398,3 +1399,72 @@ DoWithRowSelection.args = {
1398
1399
  selectable: true,
1399
1400
  selectedItems: [],
1400
1401
  };
1402
+
1403
+ const players = [
1404
+ { name: 'Maignan', position: 'Gardien' },
1405
+ { name: 'Pavard', position: 'Défenseur' },
1406
+ { name: 'Upamecano', position: 'Défenseur' },
1407
+ { name: 'Hernandez', position: 'Défenseur' },
1408
+ { name: 'Camavinga', position: 'Milieu' },
1409
+ { name: 'Tchouameni', position: 'Milieu' },
1410
+ { name: 'Griezmann', position: 'Milieu' },
1411
+ { name: 'Mbappé', position: 'Attaquant' },
1412
+ ];
1413
+
1414
+ const draggableHeaders = [
1415
+ { key: 'name', label: 'Joueur' },
1416
+ { key: 'position', label: 'Poste' },
1417
+ ];
1418
+
1419
+ const DraggableTemplate = () => ({
1420
+ components: { BmsTable },
1421
+ setup() {
1422
+ const items = ref([...players]);
1423
+ const onReorder = (reordered) => { items.value = reordered; };
1424
+ return { items, onReorder, draggableHeaders };
1425
+ },
1426
+ template: `
1427
+ <div>
1428
+ <BmsTable
1429
+ :items="items"
1430
+ :headers="draggableHeaders"
1431
+ :disable-search="true"
1432
+ draggable
1433
+ @reorder="onReorder"
1434
+ />
1435
+ <pre style="margin-top: 1rem; font-size: 0.75rem; color: #666;">Ordre actuel : {{ items.map(i => i.name).join(' → ') }}</pre>
1436
+ </div>
1437
+ `,
1438
+ });
1439
+
1440
+ export const Draggable = DraggableTemplate.bind({});
1441
+ Draggable.storyName = 'Draggable (réordonnancement)';
1442
+
1443
+ const DraggableSelectableTemplate = () => ({
1444
+ components: { BmsTable },
1445
+ setup() {
1446
+ const items = ref([...players]);
1447
+ const selectedItems = ref([]);
1448
+ const onReorder = (reordered) => { items.value = reordered; };
1449
+ return { items, selectedItems, onReorder, draggableHeaders };
1450
+ },
1451
+ template: `
1452
+ <div>
1453
+ <BmsTable
1454
+ v-model:selectedItems="selectedItems"
1455
+ :items="items"
1456
+ :headers="draggableHeaders"
1457
+ :disable-search="true"
1458
+ selectable
1459
+ draggable
1460
+ @reorder="onReorder"
1461
+ />
1462
+ <pre style="margin-top: 1rem; font-size: 0.75rem; color: #666;">Ordre : {{ items.map(i => i.name).join(' → ') }}
1463
+ Sélection : {{ selectedItems.map(i => i.name).join(', ') || '—' }}</pre>
1464
+ </div>
1465
+ `,
1466
+ });
1467
+
1468
+ export const DraggableSelectable = DraggableSelectableTemplate.bind({});
1469
+ DraggableSelectable.storyName = 'Draggable + Selectable';
1470
+ DraggableSelectable.parameters = { chromatic: { disable: true } };
@@ -45,6 +45,7 @@ interface UiTableProps {
45
45
  selectableDisabled?: boolean;
46
46
  selectMode?: SelectMode.DEFAULT | SelectMode.SINGLE;
47
47
  customSearch?: (item: unknown, searchValue: string) => boolean;
48
+ draggable?: boolean;
48
49
  }
49
50
 
50
51
  const props = withDefaults(defineProps<UiTableProps>(), {
@@ -105,6 +106,7 @@ const emits = defineEmits<{
105
106
  saveFilter: [value: SavedFilter];
106
107
  filterInput: [{ filterKey: string; value: any; e: InputEvent }];
107
108
  filterChange: [{ filterKey: string; value: any }];
109
+ reorder: [items: unknown[]];
108
110
  }>();
109
111
 
110
112
  const selectedItems: Ref<unknown[]> = defineModel('selectedItems', {
@@ -124,7 +126,8 @@ const getFilteredItems = () => {
124
126
  ? (item: unknown) => props.customSearch!(item, search.value)
125
127
  : (item: unknown) => bmsDefaultSearchFilterFunction(item, search.value);
126
128
 
127
- return sortItems(filterItems(props.items).filter(applySearch));
129
+ const filtered = filterItems(props.items).filter(applySearch);
130
+ return props.draggable ? filtered : sortItems(filtered);
128
131
  };
129
132
 
130
133
  const isMounting = ref(true);
@@ -167,12 +170,27 @@ watch(route, () => {
167
170
  }
168
171
  });
169
172
 
173
+ const pendingSelectionIndices = ref<number[] | null>(null);
174
+
175
+ const onSelectionIndicesUpdate = (indices: number[]) => {
176
+ pendingSelectionIndices.value = indices;
177
+ };
178
+
170
179
  watch(
171
180
  () => props.items,
172
181
  () => {
173
182
  if (!isMounting.value) {
174
183
  items.value = getFilteredItems();
175
- selectedItems.value = [];
184
+ if (!props.draggable) {
185
+ selectedItems.value = [];
186
+ } else if (pendingSelectionIndices.value !== null) {
187
+ selectedItems.value = pendingSelectionIndices.value
188
+ .map((i) => items.value[i])
189
+ .filter(Boolean);
190
+ pendingSelectionIndices.value = null;
191
+ } else {
192
+ selectedItems.value = [];
193
+ }
176
194
  }
177
195
  },
178
196
  );
@@ -233,12 +251,15 @@ const isTableSmall = computed(() => props.mode === TableMode.SMALL);
233
251
  <UiBmsTable
234
252
  v-model:selectedItems="selectedItems"
235
253
  :loading="loading"
236
- :items="currentItems"
254
+ :items="draggable ? items : currentItems"
237
255
  :headers="headers"
238
256
  :mode="mode as TableMode"
239
257
  :hasFilters="filters.length > 0"
240
258
  :sort="sort"
241
259
  :selectable="selectable"
260
+ :draggable="draggable"
261
+ @reorder="emits('reorder', $event)"
262
+ @selectionIndicesUpdate="onSelectionIndicesUpdate"
242
263
  :selectableDisabled="selectableDisabled"
243
264
  :totalSize="totalSize"
244
265
  :selectMode="selectMode"
@@ -759,3 +759,43 @@ WithChildElements.args = {
759
759
  hasFilters: true,
760
760
  totalSize: 2,
761
761
  };
762
+
763
+ const players = [
764
+ { name: 'Maignan', position: 'Gardien' },
765
+ { name: 'Pavard', position: 'Défenseur' },
766
+ { name: 'Upamecano', position: 'Défenseur' },
767
+ { name: 'Hernandez', position: 'Défenseur' },
768
+ { name: 'Camavinga', position: 'Milieu' },
769
+ { name: 'Tchouameni', position: 'Milieu' },
770
+ { name: 'Griezmann', position: 'Milieu' },
771
+ { name: 'Mbappé', position: 'Attaquant' },
772
+ ];
773
+
774
+ const draggableHeaders = [
775
+ { key: 'name', label: 'Joueur' },
776
+ { key: 'position', label: 'Poste' },
777
+ ];
778
+
779
+ export const Draggable = () => ({
780
+ components: { UiBmsTable },
781
+ setup() {
782
+ const items = ref([...players]);
783
+ const selectedItems = ref([]);
784
+ const onReorder = (reordered) => { items.value = reordered; };
785
+ return { items, selectedItems, onReorder, draggableHeaders };
786
+ },
787
+ template: `
788
+ <div>
789
+ <UiBmsTable
790
+ v-model:selectedItems="selectedItems"
791
+ :items="items"
792
+ :headers="draggableHeaders"
793
+ :total-size="items.length"
794
+ draggable
795
+ @reorder="onReorder"
796
+ />
797
+ <pre style="margin-top: 1rem; font-size: 0.75rem; color: #666;">Ordre : {{ items.map(i => i.name).join(' → ') }}</pre>
798
+ </div>
799
+ `,
800
+ });
801
+ Draggable.storyName = 'Draggable (réordonnancement)';
@@ -43,6 +43,7 @@ interface UiBmsTableProps {
43
43
  totalSize: number;
44
44
  maxSelectedSize?: number;
45
45
  selectMode?: SelectMode;
46
+ draggable?: boolean;
46
47
  }
47
48
 
48
49
  const props = withDefaults(defineProps<UiBmsTableProps>(), {
@@ -66,8 +67,51 @@ const emits = defineEmits<{
66
67
  clickHeader: [header: TableHeader];
67
68
  selectAll: [];
68
69
  clearSelection: [];
70
+ reorder: [items: unknown[]];
71
+ selectionIndicesUpdate: [indices: number[]];
69
72
  }>();
70
73
 
74
+ // Drag & drop
75
+ const dragSourceIndex = ref<number | null>(null);
76
+ const dragTargetIndex = ref<number | null>(null);
77
+ const isDraggingDown = computed(
78
+ () =>
79
+ dragSourceIndex.value !== null &&
80
+ dragTargetIndex.value !== null &&
81
+ dragSourceIndex.value < dragTargetIndex.value,
82
+ );
83
+
84
+ const onRowDragStart = (index: number) => {
85
+ dragSourceIndex.value = index;
86
+ };
87
+
88
+ const onRowDragOver = (index: number) => {
89
+ dragTargetIndex.value = index;
90
+ };
91
+
92
+ const onRowDragEnd = () => {
93
+ if (
94
+ dragSourceIndex.value !== null &&
95
+ dragTargetIndex.value !== null &&
96
+ dragSourceIndex.value !== dragTargetIndex.value
97
+ ) {
98
+ const reordered = [...props.items];
99
+ const [moved] = reordered.splice(dragSourceIndex.value, 1);
100
+ reordered.splice(dragTargetIndex.value, 0, moved);
101
+
102
+ if (selectedItems.value.length > 0) {
103
+ const newIndices = reordered
104
+ .map((item, i) => (selectedItems.value.includes(item) ? i : -1))
105
+ .filter((i) => i !== -1);
106
+ emits('selectionIndicesUpdate', newIndices);
107
+ }
108
+
109
+ emits('reorder', reordered);
110
+ }
111
+ dragSourceIndex.value = null;
112
+ dragTargetIndex.value = null;
113
+ };
114
+
71
115
  // Pagination
72
116
  const pagination = ref<HTMLInputElement | null>(null);
73
117
  const isFocusOnPagination = () =>
@@ -290,6 +334,7 @@ onMounted(() => {
290
334
  >
291
335
  <thead ref="thead" class="bms-table__header">
292
336
  <tr class="bms-table__headers bms-table__row">
337
+ <th v-if="draggable" class="drag-handle-header"></th>
293
338
  <th v-if="selectable">
294
339
  <UiBmsInputCheckbox
295
340
  v-if="selectMode !== SelectMode.SINGLE"
@@ -304,9 +349,9 @@ onMounted(() => {
304
349
  :style="{
305
350
  '--table-cell-width': header?.width || undefined,
306
351
  }"
307
- :class="getHeaderClasses(header, sort)"
352
+ :class="getHeaderClasses(header, draggable ? { key: null, value: SortValue.default } : sort)"
308
353
  :key="header.label"
309
- @click="emits('clickHeader', header)"
354
+ @click="!draggable && emits('clickHeader', header)"
310
355
  >
311
356
  <span class="header-content">
312
357
  {{ header.label }}
@@ -323,7 +368,7 @@ onMounted(() => {
323
368
  </span>
324
369
  </BmsTooltip>
325
370
  <component
326
- v-if="header.sortable"
371
+ v-if="header.sortable && !draggable"
327
372
  :is="getSortComponent(header)"
328
373
  :size="18"
329
374
  class="header-content-sort"
@@ -334,7 +379,7 @@ onMounted(() => {
334
379
  </thead>
335
380
  <tbody class="bms-table__body">
336
381
  <template v-if="items.length">
337
- <template v-for="item in items" :key="item">
382
+ <template v-for="(item, index) in items" :key="item">
338
383
  <UiBmsTableRow
339
384
  :item="item"
340
385
  :selected-items="selectedItems"
@@ -343,7 +388,16 @@ onMounted(() => {
343
388
  :select-mode="selectMode"
344
389
  :selectable-disabled="selectableDisabled"
345
390
  :dense="mode === TableMode.DENSE || mode === TableMode.SMALL"
391
+ :draggable="draggable"
392
+ :class="{
393
+ 'bms-table__row--drag-insert-after': draggable && dragTargetIndex === index && dragSourceIndex !== index && isDraggingDown,
394
+ 'bms-table__row--drag-insert-before': draggable && dragTargetIndex === index && dragSourceIndex !== index && !isDraggingDown,
395
+ }"
346
396
  @select="onItemSelect"
397
+ @drag-start="onRowDragStart(index)"
398
+ @drag-over="onRowDragOver(index)"
399
+ @drag-end="onRowDragEnd"
400
+ @drop="onRowDragEnd"
347
401
  >
348
402
  <template v-for="cell in headers" v-slot:[cell.key]="slotData">
349
403
  <slot :name="cell.key" v-bind="slotData" />
@@ -362,6 +416,7 @@ onMounted(() => {
362
416
  :headers="filteredHeaders"
363
417
  :select-mode="selectMode"
364
418
  :selectable-disabled="selectableDisabled"
419
+ :draggable="draggable"
365
420
  @select="onItemSelect"
366
421
  >
367
422
  <template
@@ -379,9 +434,9 @@ onMounted(() => {
379
434
  <tr class="bms-table__row">
380
435
  <td
381
436
  :colspan="
382
- selectable
383
- ? filteredHeaders.length + 1
384
- : filteredHeaders.length
437
+ filteredHeaders.length +
438
+ (selectable ? 1 : 0) +
439
+ (draggable ? 1 : 0)
385
440
  "
386
441
  class="bms-table__cell bms-table__cell--empty"
387
442
  >
@@ -563,6 +618,18 @@ onMounted(() => {
563
618
  padding-top: 16px;
564
619
  }
565
620
 
621
+ .drag-handle-header {
622
+ width: 2em;
623
+ }
624
+
625
+ :deep(.bms-table__row--drag-insert-before td) {
626
+ border-top: 2px solid var(--bms-main-100);
627
+ }
628
+
629
+ :deep(.bms-table__row--drag-insert-after td) {
630
+ border-bottom: 2px solid var(--bms-main-100);
631
+ }
632
+
566
633
  .blob {
567
634
  visibility: hidden;
568
635
  --table-blob-height: 80px;
@@ -6,7 +6,20 @@
6
6
  'bms-table__row--disabled': isChildElement,
7
7
  'bms-table__row--dense': dense,
8
8
  }"
9
+ :draggable="draggable && gripped"
10
+ @dragstart="emits('dragStart')"
11
+ @dragover="draggable && ($event.preventDefault(), emits('dragOver'))"
12
+ @dragend="gripped = false; emits('dragEnd')"
13
+ @drop="draggable && ($event.preventDefault(), emits('drop'))"
9
14
  >
15
+ <td
16
+ v-if="draggable"
17
+ class="bms-table__row__cell--drag-handle"
18
+ @mousedown="!isChildElement && (gripped = true)"
19
+ @mouseup="gripped = false"
20
+ >
21
+ <GripVertical v-if="!isChildElement" class="drag-handle-icon" />
22
+ </td>
10
23
  <td v-if="selectable" class="bms-table__row__cell__checkbox">
11
24
  <BmsTooltip
12
25
  :direction="TooltipDirection.Right"
@@ -76,10 +89,10 @@ import _isEqual from 'lodash/isEqual';
76
89
  import _get from 'lodash/get';
77
90
  import UiBmsInputCheckbox from '../form/UiBmsInputCheckbox.vue';
78
91
  import BmsTooltip from '../feedback/BmsTooltip.vue';
79
- import { CornerDownRight } from 'lucide-vue-next';
92
+ import { CornerDownRight, GripVertical } from 'lucide-vue-next';
80
93
  import UiBmsTableCell from './UiBmsTableCell.vue';
81
94
  import BmsInputRadio from '../form/BmsInputRadio.vue';
82
- import { computed } from 'vue';
95
+ import { computed, ref } from 'vue';
83
96
 
84
97
  interface Props {
85
98
  item: any;
@@ -90,13 +103,23 @@ interface Props {
90
103
  selectableDisabled?: boolean;
91
104
  dense?: boolean;
92
105
  isChildElement?: boolean;
106
+ draggable?: boolean;
93
107
  }
94
108
  const props = withDefaults(defineProps<Props>(), {
95
109
  dense: false,
96
110
  selectableDisabled: false,
111
+ draggable: false,
97
112
  });
98
113
 
99
- const emits = defineEmits<{ select: [item: any] }>();
114
+ const gripped = ref(false);
115
+
116
+ const emits = defineEmits<{
117
+ select: [item: any];
118
+ dragStart: [];
119
+ dragOver: [];
120
+ dragEnd: [];
121
+ drop: [];
122
+ }>();
100
123
 
101
124
  const currentItem = computed(() =>
102
125
  props.isChildElement ? props.item.childElement : props.item,
@@ -163,6 +186,21 @@ const getAlignClass = (header: TableHeader) => {
163
186
  margin-right: 1em;
164
187
  }
165
188
  }
189
+
190
+ &--drag-handle {
191
+ width: 2em;
192
+ cursor: default;
193
+
194
+ .drag-handle-icon {
195
+ display: block;
196
+ cursor: grab;
197
+ color: var(--bms-grey-50);
198
+
199
+ &:active {
200
+ cursor: grabbing;
201
+ }
202
+ }
203
+ }
166
204
  }
167
205
  }
168
206
  </style>