@dative-gpi/foundation-shared-components 0.0.8 → 0.0.10

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.
Files changed (140) hide show
  1. package/{models/FSButtons.ts → aliases/FSButton.ts} +24 -21
  2. package/aliases/index.ts +1 -0
  3. package/components/FSAutocompleteField.vue +207 -0
  4. package/components/FSBadge.vue +38 -0
  5. package/components/FSBreadcrumbs.vue +49 -55
  6. package/components/FSButton.vue +116 -101
  7. package/components/FSCalendar.vue +52 -39
  8. package/components/FSCalendarTwin.vue +120 -102
  9. package/components/FSCard.vue +35 -21
  10. package/components/FSCarousel.vue +63 -0
  11. package/components/FSCheckbox.vue +111 -103
  12. package/components/FSChip.vue +140 -0
  13. package/components/FSClock.vue +149 -15
  14. package/components/FSCol.vue +104 -98
  15. package/components/FSColor.vue +61 -64
  16. package/components/FSColorIcon.vue +67 -0
  17. package/components/FSContainer.vue +64 -0
  18. package/components/FSDateField.vue +211 -0
  19. package/components/FSDateRangeField.vue +225 -0
  20. package/components/FSDateTimeField.vue +257 -0
  21. package/components/FSDateTimeRangeField.vue +286 -0
  22. package/components/FSDialog.vue +103 -0
  23. package/components/FSDivider.vue +39 -0
  24. package/components/FSFadeOut.vue +49 -59
  25. package/components/FSFileButton.vue +245 -0
  26. package/components/FSHeaderButton.vue +17 -0
  27. package/components/FSIcon.vue +23 -23
  28. package/components/FSIconField.vue +232 -0
  29. package/components/FSImage.vue +142 -0
  30. package/components/FSLoadTile.vue +93 -0
  31. package/components/FSNumberField.vue +51 -53
  32. package/components/FSPasswordField.vue +99 -101
  33. package/components/FSRadio.vue +107 -109
  34. package/components/FSRadioGroup.vue +55 -57
  35. package/components/FSRemoveDialog.vue +123 -0
  36. package/components/FSRichTextField.vue +26 -33
  37. package/components/FSRow.vue +110 -104
  38. package/components/FSSearchField.vue +35 -27
  39. package/components/FSSelectField.vue +188 -0
  40. package/components/FSSlideGroup.vue +45 -49
  41. package/components/FSSlider.vue +31 -33
  42. package/components/FSSpan.vue +53 -37
  43. package/components/FSSubmitDialog.vue +165 -0
  44. package/components/FSSwitch.vue +110 -109
  45. package/components/FSTab.vue +61 -61
  46. package/components/FSTabs.vue +53 -55
  47. package/components/FSTag.vue +88 -84
  48. package/components/FSTagField.vue +32 -36
  49. package/components/FSTagGroup.vue +38 -45
  50. package/components/FSText.vue +74 -64
  51. package/components/FSTextArea.vue +187 -185
  52. package/components/FSTextField.vue +18 -20
  53. package/components/FSTile.vue +90 -0
  54. package/components/FSToggleSet.vue +282 -0
  55. package/components/FSTooltip.vue +21 -0
  56. package/components/FSWindow.vue +26 -16
  57. package/components/FSWrapGroup.vue +44 -47
  58. package/components/deviceOrganisations/FSConnectivity.vue +114 -0
  59. package/components/deviceOrganisations/FSStatus.vue +117 -0
  60. package/components/deviceOrganisations/FSStatusesCarousel.vue +105 -0
  61. package/components/deviceOrganisations/FSStatusesRow.vue +66 -0
  62. package/components/deviceOrganisations/FSWorstAlert.vue +165 -0
  63. package/components/lists/FSDataIteratorGroup.vue +7 -0
  64. package/components/lists/FSDataIteratorItem.vue +103 -0
  65. package/components/lists/FSDataTableUI.vue +982 -0
  66. package/components/lists/FSFilterButton.vue +177 -0
  67. package/components/lists/FSHeaderButton.vue +99 -0
  68. package/components/lists/FSHiddenButton.vue +81 -0
  69. package/components/tiles/FSDeviceOrganisationTileUI.vue +232 -0
  70. package/components/tiles/FSGroupTileUI.vue +192 -0
  71. package/composables/index.ts +1 -1
  72. package/composables/useBreakpoints.ts +23 -4
  73. package/composables/useColors.ts +53 -23
  74. package/composables/useSlots.ts +43 -0
  75. package/index.ts +6 -0
  76. package/models/breadcrumbs.ts +8 -0
  77. package/models/colors.ts +17 -0
  78. package/models/deviceAlerts.ts +10 -0
  79. package/models/deviceConnectivities.ts +11 -0
  80. package/models/deviceStatuses.ts +16 -0
  81. package/models/index.ts +9 -0
  82. package/models/modelStatuses.ts +11 -0
  83. package/models/rules.ts +50 -0
  84. package/models/tables.ts +33 -0
  85. package/models/toggleSets.ts +7 -0
  86. package/package.json +6 -4
  87. package/plugins/colorPlugin.ts +2 -2
  88. package/shims-plugin.d.ts +1 -1
  89. package/styles/components/fs_autocomplete_field.scss +123 -0
  90. package/styles/components/fs_button.scss +4 -6
  91. package/styles/components/fs_calendar.scss +24 -1
  92. package/styles/components/fs_card.scss +2 -5
  93. package/styles/components/fs_carousel.scss +4 -0
  94. package/styles/components/fs_chip.scss +33 -0
  95. package/styles/components/fs_clock.scss +43 -0
  96. package/styles/components/fs_col.scss +2 -0
  97. package/styles/components/fs_color_icon.scss +37 -0
  98. package/styles/components/fs_container.scss +16 -0
  99. package/styles/components/fs_data_iterator_item.scss +19 -0
  100. package/styles/components/fs_data_table.scss +97 -0
  101. package/styles/components/fs_date_field.scss +8 -0
  102. package/styles/components/fs_dialog.scss +30 -0
  103. package/styles/components/fs_divider.scss +5 -0
  104. package/styles/components/fs_fade_out.scss +10 -2
  105. package/styles/components/fs_filter_button.scss +21 -0
  106. package/styles/components/fs_header_button.scss +4 -0
  107. package/styles/components/fs_hidden_button.scss +12 -0
  108. package/styles/components/fs_icon.scss +19 -3
  109. package/styles/components/fs_icon_field.scss +12 -0
  110. package/styles/components/fs_image.scss +7 -0
  111. package/styles/components/fs_load_tile.scss +49 -0
  112. package/styles/components/fs_password_field.scss +2 -2
  113. package/styles/components/fs_row.scss +4 -1
  114. package/styles/components/fs_select_field.scss +71 -0
  115. package/styles/components/fs_slide_group.scss +6 -0
  116. package/styles/components/fs_slider.scss +29 -9
  117. package/styles/components/fs_span.scss +8 -0
  118. package/styles/components/fs_submit_dialog.scss +9 -0
  119. package/styles/components/fs_tabs.scss +4 -0
  120. package/styles/components/fs_tag_field.scss +0 -11
  121. package/styles/components/fs_text_field.scss +23 -15
  122. package/styles/components/fs_tile.scss +33 -0
  123. package/styles/components/fs_tooltip.scss +5 -0
  124. package/styles/components/fs_wrap_group.scss +7 -8
  125. package/styles/components/index.scss +22 -1
  126. package/styles/globals/breakpoints.scss +7 -0
  127. package/styles/globals/overrides.scss +20 -7
  128. package/styles/globals/text_fonts.scss +8 -8
  129. package/themes/default.ts +1 -11
  130. package/utils/css.ts +11 -0
  131. package/utils/icons.ts +75416 -0
  132. package/utils/index.ts +5 -1
  133. package/utils/levenshtein.ts +97 -0
  134. package/utils/sort.ts +9 -0
  135. package/components/FSDatePicker.vue +0 -226
  136. package/composables/useDates.ts +0 -39
  137. package/models/FSTags.ts +0 -8
  138. package/models/FSTextFields.ts +0 -23
  139. package/styles/components/fs_date_picker.scss +0 -0
  140. /package/utils/{FSRichTextField.ts → lexical.ts} +0 -0
@@ -0,0 +1,982 @@
1
+ <template>
2
+ <FSCol
3
+ gap="16px"
4
+ >
5
+ <FSRow
6
+ align="bottom-center"
7
+ >
8
+ <FSSearchField
9
+ prependInnerIcon="mdi-magnify"
10
+ :hideHeader="true"
11
+ v-model="innerSearch"
12
+ />
13
+ <FSButton
14
+ prependIcon="mdi-filter-variant"
15
+ :variant="showFilters ? 'full' : 'standard'"
16
+ @click="showFilters = !showFilters"
17
+ />
18
+ <slot name="toolbar" />
19
+ <v-spacer />
20
+ <FSToggleSet
21
+ v-if="!$props.disableTable && !$props.disableIterator"
22
+ :values="modeOptions"
23
+ :required="true"
24
+ v-model="innerMode"
25
+ />
26
+ </FSRow>
27
+ <FSRow
28
+ v-if="(showFilters && filterableHeaders.length > 0) || hiddenHeaders.length > 0"
29
+ >
30
+ <template v-if="showFilters">
31
+ <FSFilterButton
32
+ v-for="(header, index) in filterableHeaders"
33
+ :key="index"
34
+ :header="header"
35
+ :filters="filters[header.value]"
36
+ @update:filter="(value) => toggleFilter(header.value, value)"
37
+ >
38
+ <template #default="{ filter }">
39
+ <slot :name="filterSlot(header)" v-bind="{ filter }" />
40
+ </template>
41
+ </FSFilterButton>
42
+ <FSChip
43
+ v-if="resetable"
44
+ variant="standard"
45
+ :label="$tr('ui.data-table.reset-filters', 'Reset')"
46
+ :color="ColorEnum.Error"
47
+ :editable="true"
48
+ @click="resetFilter"
49
+ />
50
+ </template>
51
+ <FSHiddenButton
52
+ v-if="hiddenHeaders.length > 0"
53
+ :headers="hiddenHeaders"
54
+ :color="$props.color"
55
+ @update:show="(value) => updateHeader(value, 'hidden', false)"
56
+ />
57
+ </FSRow>
58
+ <template v-if="innerMode === 'table'">
59
+ <v-data-table
60
+ v-if="!isExtraSmall"
61
+ selectStrategy="all"
62
+ :itemValue="$props.itemValue"
63
+ :showSelect="$props.showSelect"
64
+ :singleSelect="$props.singleSelect"
65
+ :headers="extraHeaders.concat(innerHeaders)"
66
+ :groupBy="$props.groupBy ? [$props.groupBy] : []"
67
+ :sortBy="innerSortBy ? [innerSortBy] : []"
68
+ :items="innerItems"
69
+ :fixedHeader="true"
70
+ :multiSort="false"
71
+ :hover="true"
72
+ :style="style"
73
+ :class="classes"
74
+ :page="innerPage"
75
+ :itemsPerPage="innerRowsPerPage"
76
+ :modelValue="innerValue"
77
+ @auxclick:row="onClickRow"
78
+ @click:row="onClickRow"
79
+ @update:sortBy="innerSortBy = $event ? $event[0] : null"
80
+ >
81
+ <template #no-data>
82
+ <FSText
83
+ font="text-overline"
84
+ >
85
+ {{ $tr("ui.data-table.empty", "No data") }}
86
+ </FSText>
87
+ </template>
88
+ <template #[`header.data-table-select`]="props">
89
+ <FSRow
90
+ v-if="!$props.singleSelect"
91
+ class="fs-data-table-select"
92
+ align="bottom-center"
93
+ width="hug"
94
+ >
95
+ <FSCheckbox
96
+ :modelValue="props.allSelected"
97
+ :indeterminate="props.someSelected && !props.allSelected"
98
+ @update:modelValue="toggleSelectAll(props.allSelected)"
99
+ />
100
+ </FSRow>
101
+ </template>
102
+ <template #[`item.data-table-select`]="props">
103
+ <FSRow
104
+ class="fs-data-table-select"
105
+ align="bottom-center"
106
+ width="hug"
107
+ >
108
+ <FSCheckbox
109
+ :modelValue="innerValue.includes(props.item[$props.itemValue])"
110
+ @update:modelValue="toggleSelect(props.item)"
111
+ />
112
+ </FSRow>
113
+ </template>
114
+ <template #[`header.data-table-group`]="props">
115
+ <slot name="header.data-table-group" v-bind="props" />
116
+ </template>
117
+ <template #[`item.data-table-group`]="props">
118
+ <slot name="item.data-table-group" v-bind="props" />
119
+ </template>
120
+ <template #group-header="props">
121
+ <template :ref="() => { if (!props.isGroupOpen(props.item)) { props.toggleGroup(props.item) } }" />
122
+ <tr class="fs-data-table-group-header">
123
+ <td />
124
+ <td
125
+ class="fs-data-table-group-header"
126
+ :colspan="extraHeaders.concat(innerHeaders).length + 1"
127
+ >
128
+ <slot name="group-header" v-bind="props">
129
+ <FSCard
130
+ padding="12px 16px"
131
+ >
132
+ <FSRow
133
+ align="center-left"
134
+ width="hug"
135
+ >
136
+ <FSText>
137
+ <slot name="group-header-title" v-bind="props">
138
+ {{ props.item.value }}
139
+ </slot>
140
+ </FSText>
141
+ </FSRow>
142
+ </FSCard>
143
+ </slot>
144
+ </td>
145
+ </tr>
146
+ </template>
147
+ <template v-for="(header, index) in headersSlots" #[header.slotName]="props">
148
+ <slot :name="header.slotName" v-bind="props">
149
+ <FSRow
150
+ align="center-left"
151
+ :wrap="false"
152
+ :key="index"
153
+ >
154
+ <slot :name="`${header.slotName}-prepend`" />
155
+ <slot :name="`${header.slotName}-title`">
156
+ <FSText>
157
+ {{ header.text }}
158
+ </FSText>
159
+ </slot>
160
+ <slot :name="`${header.slotName}-append`" />
161
+ <v-spacer />
162
+ <slot :name="`${header.slotName}-configuration`">
163
+ <FSHeaderButton
164
+ :first="index === 0"
165
+ :last="index === headersSlots.length - 1"
166
+ @update:hide="updateHeader(header, 'hidden', !header.hidden)"
167
+ @update:left="updateHeader(header, 'index', -1)"
168
+ @update:right="updateHeader(header, 'index', 1)"
169
+ />
170
+ <FSButton
171
+ v-if="header.sortable"
172
+ variant="icon"
173
+ :color="sortColor(header, props)"
174
+ :icon="sortIcon(header, props)"
175
+ />
176
+ </slot>
177
+ </FSRow>
178
+ </slot>
179
+ </template>
180
+ <template v-for="(item, index) in itemsSlots" #[item.slotName]="props">
181
+ <slot :name="item.slotName" v-bind="props">
182
+ <FSRow
183
+ align="center-left"
184
+ :key="index"
185
+ >
186
+ <FSText
187
+ font="text-overline"
188
+ >
189
+ {{ props.item[item.value] }}
190
+ </FSText>
191
+ </FSRow>
192
+ </slot>
193
+ </template>
194
+ <template #bottom>
195
+ <FSRow
196
+ class="fs-data-table-footer"
197
+ align="center-right"
198
+ padding="16px"
199
+ gap="24px"
200
+ >
201
+ <template v-if="$props.modelValue.length">
202
+ <template v-if="$props.modelValue.length >= innerItems.length">
203
+ <FSRow
204
+ gap="2px"
205
+ >
206
+ <FSText
207
+ font="text-button"
208
+ >
209
+ {{ $tr("ui.data-table.all-selected-bold", "Warning:") }}
210
+ </FSText>
211
+ <FSText>
212
+ {{ $tr("ui.data-table.all-selected-regular", "All elements selected") }}
213
+ </FSText>
214
+ </FSRow>
215
+ </template>
216
+ <template v-else>
217
+ <FSText>
218
+ {{ $tr("ui.data-table.some-selected", "{0} element(s) selected", $props.modelValue.length.toString()) }}
219
+ </FSText>
220
+ </template>
221
+ </template>
222
+ <v-spacer />
223
+ <FSRow
224
+ align="center-right"
225
+ :wrap="false"
226
+ >
227
+ <FSText
228
+ font="text-overline"
229
+ >
230
+ {{ $tr("ui.data-table.rows-per-page", "Rows per page") }}
231
+ </FSText>
232
+ <FSRow
233
+ width="120px"
234
+ >
235
+ <FSSelectField
236
+ :clearable="false"
237
+ :hideHeader="true"
238
+ :items="rowsPerPageOptions"
239
+ v-model="innerRowsPerPage"
240
+ />
241
+ </FSRow>
242
+ </FSRow>
243
+ <FSToggleSet
244
+ v-if="innerRowsPerPage !== -1"
245
+ class="fs-data-table-pagination"
246
+ variant="slide"
247
+ :values="pageOptions"
248
+ :required="true"
249
+ v-model="innerPage"
250
+ />
251
+ </FSRow>
252
+ </template>
253
+ <template v-for="(_, name) in innerSlots" #[name]="props">
254
+ <slot :name="name" v-bind="props" />
255
+ </template>
256
+ </v-data-table>
257
+ <v-data-iterator
258
+ v-else
259
+ class="fs-data-table-iterator"
260
+ :items="innerItems"
261
+ :page="innerPage"
262
+ :itemsPerPage="innerRowsPerPage"
263
+ >
264
+ <template #default="{ items }">
265
+ <FSCol
266
+ width="fill"
267
+ >
268
+ <template v-for="(item, index) in items">
269
+ <slot name="item.iterator" v-bind="{ item, index }">
270
+ <FSDataIteratorItem
271
+ v-if="item.type === 'item'"
272
+ :key="index"
273
+ :item="item.raw"
274
+ :color="$props.color"
275
+ :itemTo="$props.itemTo"
276
+ :showSelect="$props.showSelect"
277
+ :headers="innerHeaders.filter(h => !$props.sneakyHeaders.includes(h.value))"
278
+ :modelValue="innerValue.includes(item.raw[$props.itemValue])"
279
+ @update:modelValue="toggleSelect"
280
+ >
281
+ <template #[`item.top`]="props">
282
+ <slot name="item.top" v-bind="props" />
283
+ </template>
284
+ <template v-for="(item, index) in itemsSlots" #[item.slotName]="props">
285
+ <slot :name="item.slotName" v-bind="props">
286
+ <FSText
287
+ :key="index"
288
+ >
289
+ {{ props.item[item.value] }}
290
+ </FSText>
291
+ </slot>
292
+ </template>
293
+ <template #[`item.bottom`]="props">
294
+ <slot name="item.bottom" v-bind="props" />
295
+ </template>
296
+ </FSDataIteratorItem>
297
+ </slot>
298
+ </template>
299
+ </FSCol>
300
+ </template>
301
+ <template #footer>
302
+ <FSRow
303
+ class="fs-data-table-footer"
304
+ align="center-right"
305
+ padding="16px"
306
+ gap="24px"
307
+ >
308
+ <template v-if="$props.modelValue.length">
309
+ <template v-if="$props.modelValue.length >= innerItems.length">
310
+ <FSRow
311
+ gap="2px"
312
+ >
313
+ <FSText
314
+ font="text-button"
315
+ >
316
+ {{ $tr("ui.data-table.all-selected-bold", "Attention:") }}
317
+ </FSText>
318
+ <FSText>
319
+ {{ $tr("ui.data-table.all-selected-regular", "All elements selected") }}
320
+ </FSText>
321
+ </FSRow>
322
+ </template>
323
+ <template v-else>
324
+ <FSText>
325
+ {{ $tr("ui.data-table.some-selected", "{0} element(s) selected", $props.modelValue.length.toString()) }}
326
+ </FSText>
327
+ </template>
328
+ </template>
329
+ <v-spacer />
330
+ <FSRow
331
+ align="center-right"
332
+ :wrap="false"
333
+ >
334
+ <FSText
335
+ font="text-overline"
336
+ >
337
+ {{ $tr("ui.data-table.rows-per-page", "Rows per page") }}
338
+ </FSText>
339
+ <FSRow
340
+ width="120px"
341
+ >
342
+ <FSSelectField
343
+ :clearable="false"
344
+ :hideHeader="true"
345
+ :items="rowsPerPageOptions"
346
+ v-model="innerRowsPerPage"
347
+ />
348
+ </FSRow>
349
+ </FSRow>
350
+ <FSToggleSet
351
+ v-if="innerRowsPerPage !== -1"
352
+ class="fs-data-table-pagination"
353
+ variant="slide"
354
+ :values="pageOptions"
355
+ :required="true"
356
+ v-model="innerPage"
357
+ />
358
+ </FSRow>
359
+ </template>
360
+ </v-data-iterator>
361
+ </template>
362
+ <template v-else-if="innerMode === 'iterator'">
363
+ <v-data-iterator
364
+ class="fs-data-table-iterator"
365
+ :items="innerItems"
366
+ :itemsPerPage="size"
367
+ >
368
+ <template #default="{ items }">
369
+ <FSRow
370
+ width="hug"
371
+ >
372
+ <template v-for="(item, index) in items">
373
+ <FSDataIteratorItem
374
+ v-if="item.type === 'item'"
375
+ :key="index"
376
+ :item="item.raw"
377
+ :color="$props.color"
378
+ :itemTo="$props.itemTo"
379
+ :showSelect="$props.showSelect"
380
+ :headers="innerHeaders.filter(h => !$props.sneakyHeaders.includes(h.value))"
381
+ :modelValue="innerValue.includes(item.raw[$props.itemValue])"
382
+ @update:modelValue="toggleSelect"
383
+ >
384
+ <template #[`item.top`]="props">
385
+ <slot name="item.top" v-bind="props" />
386
+ </template>
387
+ <template v-for="(item, index) in itemsSlots" #[item.slotName]="props">
388
+ <slot :name="item.slotName" v-bind="props">
389
+ <FSText
390
+ :key="index"
391
+ >
392
+ {{ props.item[item.value] }}
393
+ </FSText>
394
+ </slot>
395
+ </template>
396
+ <template #[`item.bottom`]="props">
397
+ <slot name="item.bottom" v-bind="props" />
398
+ </template>
399
+ </FSDataIteratorItem>
400
+ </template>
401
+ </FSRow>
402
+ </template>
403
+ </v-data-iterator>
404
+ </template>
405
+ <div class="fs-data-table-intersection" />
406
+ </FSCol>
407
+ </template>
408
+
409
+ <script lang="ts">
410
+ import { computed, defineComponent, getCurrentInstance, onMounted, onUnmounted, PropType, ref, Slot, watch } from "vue";
411
+ import { useRouter } from "vue-router";
412
+
413
+ import { ColorEnum, FSDataTableColumn, FSDataTableFilter, FSDataTableOrder, FSToggle } from "@dative-gpi/foundation-shared-components/models";
414
+ import { useBreakpoints, useColors, useSlots } from "@dative-gpi/foundation-shared-components/composables";
415
+ import { useTranslationsProvider } from "@dative-gpi/foundation-shared-services/composables";
416
+
417
+ import FSDataIteratorItem from "./FSDataIteratorItem.vue";
418
+ import FSFilterButton from "./FSFilterButton.vue";
419
+ import FSHiddenButton from "./FSHiddenButton.vue";
420
+ import FSHeaderButton from "./FSHeaderButton.vue";
421
+ import FSSearchField from "../FSSearchField.vue";
422
+ import FSSelectField from "../FSSelectField.vue";
423
+ import FSContainer from "../FSContainer.vue";
424
+ import FSToggleSet from "../FSToggleSet.vue";
425
+ import FSCheckbox from "../FSCheckbox.vue";
426
+ import FSCard from "../FSCard.vue";
427
+ import FSChip from "../FSChip.vue";
428
+ import FSText from "../FSText.vue";
429
+ import FSRow from "../FSRow.vue";
430
+ import FSCol from "../FSCol.vue";
431
+
432
+ export default defineComponent({
433
+ name: "FSDataTableUI",
434
+ components: {
435
+ FSDataIteratorItem,
436
+ FSFilterButton,
437
+ FSHiddenButton,
438
+ FSHeaderButton,
439
+ FSSearchField,
440
+ FSSelectField,
441
+ FSContainer,
442
+ FSToggleSet,
443
+ FSCheckbox,
444
+ FSCard,
445
+ FSChip,
446
+ FSText,
447
+ FSRow,
448
+ FSCol
449
+ },
450
+ props: {
451
+ headers: {
452
+ type: Array as PropType<FSDataTableColumn[]>,
453
+ required: true
454
+ },
455
+ filters: {
456
+ type: Object as PropType<{ [key: string]: FSDataTableFilter[] }>,
457
+ required: false,
458
+ default: () => ({})
459
+ },
460
+ sneakyHeaders: {
461
+ type: Array as PropType<string[]>,
462
+ required: false,
463
+ default: () => []
464
+ },
465
+ items: {
466
+ type: Array as PropType<any[]>,
467
+ required: true
468
+ },
469
+ itemValue: {
470
+ type: String,
471
+ required: false,
472
+ default: "id"
473
+ },
474
+ itemTo: {
475
+ type: Function,
476
+ required: false,
477
+ default: null
478
+ },
479
+ rowsPerPage: {
480
+ type: Number,
481
+ required: false,
482
+ default: 10
483
+ },
484
+ page: {
485
+ type: Number,
486
+ required: false,
487
+ default: 1
488
+ },
489
+ groupBy: {
490
+ type: Object as PropType<FSDataTableOrder>,
491
+ required: false,
492
+ default: null
493
+ },
494
+ sortBy: {
495
+ type: Object as PropType<FSDataTableOrder>,
496
+ required: false,
497
+ default: null
498
+ },
499
+ modelValue: {
500
+ type: Array as PropType<string[]>,
501
+ required: false,
502
+ default: () => []
503
+ },
504
+ color: {
505
+ type: String as PropType<ColorEnum>,
506
+ required: false,
507
+ default: ColorEnum.Primary
508
+ },
509
+ showSelect: {
510
+ type: Boolean,
511
+ required: false,
512
+ default: true
513
+ },
514
+ singleSelect: {
515
+ type: Boolean,
516
+ required: false,
517
+ default: false
518
+ },
519
+ selectedOnly: {
520
+ type: Boolean,
521
+ required: false,
522
+ default: false
523
+ },
524
+ mode: {
525
+ type: String as PropType<"table" | "iterator">,
526
+ required: false,
527
+ default: "table"
528
+ },
529
+ disableTable: {
530
+ type: Boolean,
531
+ required: false,
532
+ default: false
533
+ },
534
+ disableIterator: {
535
+ type: Boolean,
536
+ required: false,
537
+ default: false
538
+ },
539
+ sizeIterator: {
540
+ type: Number,
541
+ required: false,
542
+ default: 20
543
+ }
544
+ },
545
+ emits: ["update:modelValue", "update:headers", "update:filters", "update:mode", "update:sortBy", "update:rowsPerPage", "update:page", "click:row"],
546
+ setup(props, { emit }) {
547
+ const { isExtraSmall } = useBreakpoints();
548
+ const { $tr } = useTranslationsProvider();
549
+ const router = useRouter();
550
+
551
+ const backgrounds = useColors().getColors(ColorEnum.Background);
552
+ const lights = useColors().getColors(ColorEnum.Light);
553
+
554
+ const filters = ref<{ [key: string]: FSDataTableFilter[] }>({});
555
+ const innerRowsPerPage = ref(props.rowsPerPage);
556
+ const innerValue = ref(props.modelValue);
557
+ const innerSortBy = ref(props.sortBy);
558
+ const innerMode = ref(props.mode);
559
+ const innerPage = ref(props.page);
560
+ const innerSearch = ref(null);
561
+ const showFilters = ref(true);
562
+ const resetable = ref(false);
563
+
564
+ const intersectionObserver = ref<IntersectionObserver | null>(null);
565
+ const size = ref(props.sizeIterator);
566
+
567
+ const modeOptions: FSToggle[] = [
568
+ { id: "table", prependIcon: "mdi-table" },
569
+ { id: "iterator", prependIcon: "mdi-apps" }
570
+ ];
571
+
572
+ const rowsPerPageOptions: { id: number, label: string }[] = [
573
+ { id: 10, label: "10" },
574
+ { id: 30, label: "30" },
575
+ { id: -1, label: $tr("ui.data-table.all-rows", "All") }
576
+ ];
577
+
578
+ const innerSlots = computed((): { [label: string]: Slot<any> } => {
579
+ const slots = { ...useSlots().slots };
580
+ delete slots["toolbar"];
581
+ delete slots["no-data"];
582
+ delete slots["header.data-table-select"];
583
+ delete slots["item.data-table-select"];
584
+ delete slots["header.data-table-group"];
585
+ delete slots["item.data-table-group"];
586
+ delete slots["group-header"];
587
+ delete slots["group-header-title"];
588
+ delete slots["bottom"];
589
+ for (const header of filterableHeaders.value) {
590
+ delete slots[filterSlot(header)];
591
+ }
592
+ for (const header of headersSlots.value) {
593
+ delete slots[header.slotName];
594
+ delete slots[header.slotName + "-prepend"];
595
+ delete slots[header.slotName + "-title"];
596
+ delete slots[header.slotName + "-append"];
597
+ delete slots[header.slotName + "-configuration"];
598
+ }
599
+ for (const item of itemsSlots.value) {
600
+ delete slots[item.slotName];
601
+ }
602
+ return slots;
603
+ });
604
+
605
+ const style = computed((): { [code: string]: string } & Partial<CSSStyleDeclaration> => {
606
+ return {
607
+ "--fs-data-table-background-color" : backgrounds.base,
608
+ "--fs-data-table-border-color" : lights.base
609
+ };
610
+ });
611
+
612
+ const classes = computed((): string[] => {
613
+ const innerClasses = ["fs-data-table"];
614
+ if (props.groupBy) {
615
+ innerClasses.push("fs-data-table-grouped");
616
+ }
617
+ return innerClasses;
618
+ });
619
+
620
+ const extraHeaders = computed((): any[] => {
621
+ const extra = [];
622
+ if (props.groupBy) {
623
+ extra.push({
624
+ key: "data-table-group",
625
+ width: "0%"
626
+ });
627
+ }
628
+ if (props.showSelect) {
629
+ extra.push({
630
+ key: "data-table-select",
631
+ width: "0%"
632
+ });
633
+ }
634
+ return extra;
635
+ });
636
+
637
+ const innerHeaders = computed((): FSDataTableColumn[] => {
638
+ return props.headers.filter(c => !c.hidden)
639
+ .sort((c1, c2) => c1.index - c2.index).map((c) => {
640
+ if (c.sort) {
641
+ return c;
642
+ }
643
+ return {
644
+ ...c,
645
+ sort: (a: any, b: any): number => JSON.stringify(a)
646
+ .localeCompare(JSON.stringify(b), undefined, { numeric: true })
647
+ };
648
+ })
649
+ });
650
+
651
+ const hiddenHeaders = computed((): FSDataTableColumn[] => {
652
+ return props.headers.filter(c => c.hidden);
653
+ });
654
+
655
+ const filterableHeaders = computed((): FSDataTableColumn[] => {
656
+ return innerHeaders.value.filter((c) => c.filterable);
657
+ });
658
+
659
+ const innerItems = computed((): any[] => {
660
+ const activeFilters: { key: string, filter: FSDataTableFilter }[] = Object.keys(filters.value).reduce((acc, key) => {
661
+ return acc.concat(filters.value[key].filter((filter) => filter.hidden).map((filter) => ({ key, filter })));
662
+ }, []);
663
+ return props.items.filter((item) => {
664
+ if (props.selectedOnly && !innerValue.value.includes(item[props.itemValue])) {
665
+ return false;
666
+ }
667
+ if (innerSearch.value) {
668
+ if (!JSON.stringify(item).toLowerCase().includes(innerSearch.value.toString().toLowerCase())) {
669
+ return false;
670
+ }
671
+ }
672
+ if (activeFilters.some(af => af.filter.filter(af.filter.value, item[af.key], item))) {
673
+ return false;
674
+ }
675
+ return true;
676
+ });
677
+ });
678
+
679
+ const headersSlots = computed((): FSDataTableColumn[] => {
680
+ return innerHeaders.value.map((c) => ({ ...c, slotName: `header.${c.value}` }));
681
+ });
682
+
683
+ const itemsSlots = computed((): FSDataTableColumn[] => {
684
+ return innerHeaders.value.map((c) => ({ ...c, slotName: `item.${c.value}` }));
685
+ });
686
+
687
+ const groups = computed((): { [key: string]: any[] } => {
688
+ if (props.groupBy) {
689
+ return innerItems.value.reduce((acc, item) => {
690
+ const key = item[props.groupBy.key];
691
+ if (!acc[key]) {
692
+ acc[key] = [];
693
+ }
694
+ acc[key].push(item);
695
+ return acc;
696
+ }, {});
697
+ }
698
+ return {};
699
+ });
700
+
701
+ const pageOptions = computed((): { id: number, label: string }[] => {
702
+ if (innerRowsPerPage.value === -1) {
703
+ return [];
704
+ }
705
+ else {
706
+ const total = Math.ceil((innerItems.value.length + Object.keys(groups.value).length) / innerRowsPerPage.value);
707
+ return Array.from(Array(total).keys()).map(i => ({
708
+ id: i + 1,
709
+ label: (i + 1).toString()
710
+ }));
711
+ }
712
+ });
713
+
714
+ const toggleSelectAll = (allSelected: boolean): void => {
715
+ if (allSelected) {
716
+ innerValue.value = [];
717
+ }
718
+ else {
719
+ innerValue.value = innerItems.value.map((item) => item[props.itemValue]);
720
+ }
721
+ emit("update:modelValue", innerValue.value);
722
+ };
723
+
724
+ const toggleSelect = (item: any): void => {
725
+ const index = innerValue.value.indexOf(item[props.itemValue]);
726
+ if (index > -1) {
727
+ innerValue.value.splice(index, 1);
728
+ }
729
+ else {
730
+ innerValue.value.push(item[props.itemValue]);
731
+ }
732
+ emit("update:modelValue", innerValue.value);
733
+ };
734
+
735
+ const toggleFilter = (header: string, value: FSDataTableFilter[]): void => {
736
+ filters.value[header] = value;
737
+ emit("update:filters", filters.value);
738
+ // If a filter is hidden, the reset button will be shown
739
+ resetable.value = Object.keys(filters.value)
740
+ .some((key) => filters.value[key].some((filter) => filter.hidden));
741
+ };
742
+
743
+ const resetFilter = (): void => {
744
+ for (const key in filters.value) {
745
+ filters.value[key] = filters.value[key].map((filter) => ({ ...filter, hidden: false }));
746
+ }
747
+ emit("update:filters", filters.value);
748
+ resetable.value = false;
749
+ };
750
+
751
+ const filterSlot = (header: FSDataTableColumn): string => {
752
+ return `filter.${header.value}`;
753
+ };
754
+
755
+ const updateHeader = (header: FSDataTableColumn, property: "hidden" | "index", value: boolean | number) => {
756
+ const innerColumns = props.headers.slice(0);
757
+ const innerColumn = innerColumns.find((column) => column.value === header.value);
758
+ if (innerColumn) {
759
+ switch (property) {
760
+ case "hidden":
761
+ innerColumn.hidden = value as boolean;
762
+ break;
763
+ case "index":
764
+ switch (value) {
765
+ case 1:
766
+ for (const column of innerColumns) {
767
+ if (column.index === header.index + 1) {
768
+ column.index--;
769
+ }
770
+ }
771
+ break;
772
+ case -1:
773
+ for (const column of innerColumns) {
774
+ if (column.index === header.index - 1) {
775
+ column.index++;
776
+ }
777
+ }
778
+ break;
779
+ }
780
+ innerColumn.index += value as number;
781
+ innerColumns.sort((c1, c2) => c1.index - c2.index);
782
+ for (let i = 0; i < innerColumns.length; i++) {
783
+ innerColumns[i].index = i;
784
+ }
785
+ break;
786
+ }
787
+ emit("update:headers", innerColumns);
788
+ }
789
+ };
790
+
791
+ const computeFilters = (): void => {
792
+ const filterDictionary: { [key: string]: FSDataTableFilter[] } = {};
793
+
794
+ for (const header of innerHeaders.value.filter((h) => h.filterable)) {
795
+ const key = header.value!;
796
+ const currentFilters = filters.value[key];
797
+
798
+ let value: FSDataTableFilter[] = [];
799
+
800
+ if (header.fixedFilters) {
801
+ value = header.fixedFilters.map((ff): FSDataTableFilter => ({
802
+ hidden: currentFilters?.find((cf) => cf.value == (ff.value || null))?.hidden ?? false,
803
+ text: ff.text?.toString() ?? "—",
804
+ value: ff.value || null,
805
+ filter: header.methodFilter ?? ((value, property) => {
806
+ property = [property].flat();
807
+ return Array.isArray(property) ? property.includes(value) || (!value && property.length == 0) : (!value && !property) || value == property;
808
+ })
809
+ }));
810
+ }
811
+ else {
812
+ const mapToInnerValue = header.innerValue ? header.innerValue : (i: any) => i;
813
+ const itemValues = props.items.flatMap((item) => Array.isArray(item[key]) && item[key].length == 0 ? undefined : item[key]).map(mapToInnerValue);
814
+ const distinctValues = [...new Set(itemValues)];
815
+
816
+ value = distinctValues.map((dv): FSDataTableFilter => ({
817
+ hidden: currentFilters?.find((cf) => cf.value == (dv || null))?.hidden ?? false,
818
+ text: dv?.toString() ?? "—",
819
+ value: dv || null,
820
+ filter: header.methodFilter ?? ((_, property) => {
821
+ property = [property].flat().map(mapToInnerValue);
822
+ return Array.isArray(property) ? property.includes(dv) || (!dv && property.length == 0) : (!dv && !property) || dv == property;
823
+ })
824
+ }));
825
+ }
826
+ filterDictionary[key] = value.sort((v1, v2) => {
827
+ return v1.text.localeCompare(v2.text, undefined, { numeric: true });
828
+ });
829
+ }
830
+ for (const [key, filters] of Object.entries(props.filters)) {
831
+ for (const filter of filters) {
832
+ const fromDictionary = filterDictionary[key].find(f => f.value == filter.value);
833
+ if (fromDictionary) {
834
+ fromDictionary.hidden = filter.hidden;
835
+ }
836
+ }
837
+ }
838
+ filters.value = filterDictionary;
839
+ };
840
+
841
+ const sortColor = (header: FSDataTableColumn, slotProps: any) => {
842
+ const sort = slotProps.sortBy.find((s: any) => s.key == header.value);
843
+ if (sort) {
844
+ return props.color;
845
+ }
846
+ return ColorEnum.Light;
847
+ };
848
+
849
+ const sortIcon = (header: FSDataTableColumn, slotProps: any) => {
850
+ const sort = slotProps.sortBy.find((s: any) => s.key == header.value);
851
+ if (sort) {
852
+ switch (sort.order) {
853
+ case "asc": return "mdi-sort-reverse-variant";
854
+ case "desc": return "mdi-sort-variant";
855
+ }
856
+ }
857
+ return "mdi-sort-variant-off";
858
+ };
859
+
860
+ const observeIntersection = (): void => {
861
+ switch (innerMode.value) {
862
+ case "table":
863
+ if (intersectionObserver.value && document.querySelector(".fs-data-table-intersection")) {
864
+ intersectionObserver.value.unobserve(document.querySelector(".fs-data-table-intersection"));
865
+ }
866
+ return;
867
+ case "iterator":
868
+ if (!intersectionObserver.value) {
869
+ intersectionObserver.value = new IntersectionObserver(entries => {
870
+ entries.forEach((entry) => {
871
+ if (entry.boundingClientRect.bottom < window.innerHeight * 1.25) {
872
+ if (innerItems.value.length > size.value) {
873
+ size.value = Math.min(size.value + props.sizeIterator, innerItems.value.length);
874
+ }
875
+ }
876
+ });
877
+ }, { threshold: [0.9] });
878
+ }
879
+ if (document.querySelector(".fs-data-table-intersection")) {
880
+ intersectionObserver.value.observe(document.querySelector(".fs-data-table-intersection"));
881
+ }
882
+ return;
883
+ }
884
+ }
885
+
886
+ const onClickRow = computed(() => {
887
+ if (!!getCurrentInstance()?.vnode.props?.['onClick:row'] || props.itemTo) {
888
+ return (event: PointerEvent, row: any) => {
889
+ if (props.itemTo && router) {
890
+ if (event.metaKey || event.ctrlKey || event.button === 1) {
891
+ window.open(router.resolve(props.itemTo(row.item)).href, "_blank");
892
+ }
893
+ else {
894
+ router.push(props.itemTo(row.item));
895
+ }
896
+ }
897
+ else {
898
+ emit("click:row", row.item);
899
+ }
900
+ };
901
+ }
902
+ else {
903
+ return null;
904
+ }
905
+ });
906
+
907
+ onMounted(() => {
908
+ computeFilters();
909
+ observeIntersection();
910
+ });
911
+
912
+ onUnmounted(() => {
913
+ if (intersectionObserver.value) {
914
+ intersectionObserver.value.disconnect();
915
+ }
916
+ });
917
+
918
+ watch(innerSearch, () => {
919
+ innerPage.value = 1;
920
+ });
921
+
922
+ watch(innerMode, () => {
923
+ emit("update:mode", innerMode.value);
924
+ size.value = props.sizeIterator;
925
+ observeIntersection();
926
+ });
927
+
928
+ watch(innerSortBy, () => {
929
+ emit("update:sortBy", innerSortBy.value);
930
+ });
931
+
932
+ watch(innerPage, () => {
933
+ emit("update:page", innerPage.value);
934
+ });
935
+
936
+ watch(innerRowsPerPage, () => {
937
+ emit("update:rowsPerPage", innerRowsPerPage.value);
938
+ });
939
+
940
+ watch(() => props.headers, () => {
941
+ computeFilters();
942
+ });
943
+
944
+ return {
945
+ ColorEnum,
946
+ innerSlots,
947
+ rowsPerPageOptions,
948
+ innerRowsPerPage,
949
+ innerSearch,
950
+ innerMode,
951
+ modeOptions,
952
+ innerPage,
953
+ pageOptions,
954
+ showFilters,
955
+ extraHeaders,
956
+ innerHeaders,
957
+ hiddenHeaders,
958
+ filterableHeaders,
959
+ filters,
960
+ resetable,
961
+ innerSortBy,
962
+ innerItems,
963
+ headersSlots,
964
+ itemsSlots,
965
+ innerValue,
966
+ classes,
967
+ style,
968
+ size,
969
+ onClickRow,
970
+ isExtraSmall,
971
+ toggleSelectAll,
972
+ toggleSelect,
973
+ updateHeader,
974
+ toggleFilter,
975
+ resetFilter,
976
+ filterSlot,
977
+ sortColor,
978
+ sortIcon,
979
+ };
980
+ }
981
+ })
982
+ </script>