@bildvitta/quasar-ui-asteroid 3.17.0-beta.20 → 3.17.0-beta.21

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 (315) hide show
  1. package/package.json +1 -1
  2. package/src/assets/sounds/nave-notification.mp3 +0 -0
  3. package/src/asteroid.js +4 -4
  4. package/src/components/actions/QasActions.vue +81 -81
  5. package/src/components/actions/QasActions.yml +34 -34
  6. package/src/components/actions-menu/QasActionsMenu.vue +251 -251
  7. package/src/components/actions-menu/QasActionsMenu.yml +89 -89
  8. package/src/components/actions-menu/composables/use-delete.js +30 -30
  9. package/src/components/actions-menu/composables/use-double-split-actions.js +42 -42
  10. package/src/components/actions-menu/composables/use-options-actions.js +27 -27
  11. package/src/components/actions-menu/composables/use-single-action.js +17 -17
  12. package/src/components/actions-menu/composables/use-single-split-actions.js +35 -35
  13. package/src/components/actions-menu/composables/use-tooltips.js +43 -43
  14. package/src/components/actions-menu/utils/get-label.js +3 -3
  15. package/src/components/actions-menu/utils/set-click-handler.js +6 -6
  16. package/src/components/alert/QasAlert.test.js +43 -43
  17. package/src/components/alert/QasAlert.vue +110 -110
  18. package/src/components/alert/QasAlert.yml +39 -39
  19. package/src/components/app-bar/QasAppBar.vue +119 -119
  20. package/src/components/app-bar/QasAppBar.yml +44 -44
  21. package/src/components/app-menu/QasAppMenu.vue +392 -392
  22. package/src/components/app-menu/QasAppMenu.yml +73 -73
  23. package/src/components/app-menu/composables/use-app-menu-dropdown.js +71 -71
  24. package/src/components/app-menu/composables/use-app-user.js +49 -49
  25. package/src/components/app-menu/composables/use-development-badge.js +23 -23
  26. package/src/components/app-menu/private/PvAppMenuDropdown.vue +71 -71
  27. package/src/components/app-menu/private/PvAppMenuHelpChat.vue +222 -222
  28. package/src/components/app-user/QasAppUser.vue +281 -281
  29. package/src/components/app-user/QasAppUser.yml +56 -56
  30. package/src/components/avatar/QasAvatar.vue +95 -95
  31. package/src/components/avatar/QasAvatar.yml +38 -38
  32. package/src/components/avatar/enums/AvatarColors.js +10 -10
  33. package/src/components/badge/QasBadge.vue +87 -87
  34. package/src/components/badge/QasBadge.yml +55 -55
  35. package/src/components/board-generator/QasBoardGenerator.vue +712 -712
  36. package/src/components/board-generator/QasBoardGenerator.yml +167 -167
  37. package/src/components/box/QasBox.vue +51 -51
  38. package/src/components/box/QasBox.yml +29 -29
  39. package/src/components/breakline/QasBreakline.vue +38 -38
  40. package/src/components/breakline/QasBreakline.yml +25 -25
  41. package/src/components/btn/QasBtn.vue +148 -148
  42. package/src/components/btn/QasBtn.yml +48 -48
  43. package/src/components/btn-dropdown/QasBtnDropdown.vue +146 -146
  44. package/src/components/btn-dropdown/QasBtnDropdown.yml +59 -59
  45. package/src/components/card/QasCard.vue +132 -132
  46. package/src/components/card/QasCard.yml +44 -44
  47. package/src/components/card-image/QasCardImage.vue +95 -95
  48. package/src/components/card-image/QasCardImage.yml +48 -48
  49. package/src/components/chart-view/QasChartView.vue +441 -441
  50. package/src/components/chart-view/QasChartView.yml +109 -109
  51. package/src/components/chart-view/config/charts/bar.js +45 -45
  52. package/src/components/chart-view/config/charts/doughnut.js +43 -43
  53. package/src/components/chart-view/config/charts/index.js +9 -9
  54. package/src/components/chart-view/config/charts/line.js +57 -57
  55. package/src/components/chart-view/config/defaults/colors.js +19 -19
  56. package/src/components/chart-view/config/defaults/font.js +5 -5
  57. package/src/components/chart-view/config/defaults/index.js +2 -2
  58. package/src/components/chart-view/config/index.js +3 -3
  59. package/src/components/chart-view/config/plugins/index.js +3 -3
  60. package/src/components/chart-view/config/plugins/legend.js +9 -9
  61. package/src/components/chart-view/config/plugins/tooltip.js +15 -15
  62. package/src/components/chart-view/config/plugins/zoom.js +31 -31
  63. package/src/components/checkbox/QasCheckbox.vue +150 -150
  64. package/src/components/checkbox/QasCheckbox.yml +36 -36
  65. package/src/components/copy/QasCopy.vue +46 -46
  66. package/src/components/copy/QasCopy.yml +23 -23
  67. package/src/components/date/QasDate.vue +600 -600
  68. package/src/components/date/QasDate.yml +69 -69
  69. package/src/components/date/enums/DateMaskOptions.js +6 -6
  70. package/src/components/date-time-input/QasDateTimeInput.vue +269 -269
  71. package/src/components/date-time-input/QasDateTimeInput.yml +64 -64
  72. package/src/components/debugger/QasDebugger.vue +31 -31
  73. package/src/components/debugger/QasDebugger.yml +10 -10
  74. package/src/components/delete/QasDelete.vue +100 -100
  75. package/src/components/delete/QasDelete.yml +60 -60
  76. package/src/components/dialog/QasDialog.vue +217 -217
  77. package/src/components/dialog/QasDialog.yml +117 -117
  78. package/src/components/dialog/composables/use-cancel.js +40 -40
  79. package/src/components/dialog/composables/use-dynamic-components.js +86 -86
  80. package/src/components/dialog/composables/use-ok.js +45 -45
  81. package/src/components/dialog-router/QasDialogRouter.vue +86 -86
  82. package/src/components/dialog-router/QasDialogRouter.yml +23 -23
  83. package/src/components/drawer/QasDrawer.vue +118 -118
  84. package/src/components/drawer/QasDrawer.yml +58 -58
  85. package/src/components/empty-result-text/QasEmptyResultText.vue +18 -18
  86. package/src/components/empty-result-text/QasEmptyResultText.yml +14 -14
  87. package/src/components/expansion-item/QasExpansionItem.vue +291 -291
  88. package/src/components/expansion-item/QasExpansionItem.yml +76 -76
  89. package/src/components/field/QasField.vue +220 -220
  90. package/src/components/field/QasField.yml +36 -36
  91. package/src/components/filters/QasFilters.vue +472 -472
  92. package/src/components/filters/QasFilters.yml +171 -171
  93. package/src/components/filters/private/PvFiltersButton.vue +101 -101
  94. package/src/components/form-generator/QasFormGenerator.vue +274 -274
  95. package/src/components/form-generator/QasFormGenerator.yml +109 -109
  96. package/src/components/form-view/QasFormView.vue +484 -484
  97. package/src/components/form-view/QasFormView.yml +253 -253
  98. package/src/components/gallery/QasGallery.vue +243 -243
  99. package/src/components/gallery/QasGallery.yml +188 -188
  100. package/src/components/gallery/composables/use-delete.js +54 -54
  101. package/src/components/gallery/private/PvGalleryCarouselDialog.vue +92 -92
  102. package/src/components/gallery/private/PvGalleryDeleteDialog.vue +54 -54
  103. package/src/components/gallery-card/QasGalleryCard.vue +153 -153
  104. package/src/components/gallery-card/QasGalleryCard.yml +61 -61
  105. package/src/components/grabbable/QasGrabbable.vue +185 -185
  106. package/src/components/grabbable/QasGrabbable.yml +26 -26
  107. package/src/components/grid-generator/QasGridGenerator.vue +210 -210
  108. package/src/components/grid-generator/QasGridGenerator.yml +127 -127
  109. package/src/components/grid-item/QasGridItem.vue +89 -89
  110. package/src/components/grid-item/QasGridItem.yml +22 -22
  111. package/src/components/header/QasHeader.vue +152 -152
  112. package/src/components/header/QasHeader.yml +50 -50
  113. package/src/components/infinite-scroll/QasInfiniteScroll.vue +168 -168
  114. package/src/components/infinite-scroll/QasInfiniteScroll.yml +78 -78
  115. package/src/components/info/QasInfo.vue +155 -155
  116. package/src/components/info/QasInfo.yml +34 -34
  117. package/src/components/input/QasInput.vue +252 -252
  118. package/src/components/input/QasInput.yml +65 -65
  119. package/src/components/label/QasLabel.vue +61 -61
  120. package/src/components/label/QasLabel.yml +44 -44
  121. package/src/components/layout/QasLayout.vue +101 -101
  122. package/src/components/layout/QasLayout.yml +46 -46
  123. package/src/components/layout/private/PvLayoutNotificationCard.vue +86 -86
  124. package/src/components/layout/private/PvLayoutNotificationsDrawer.vue +140 -140
  125. package/src/components/list-items/QasListItems.vue +125 -125
  126. package/src/components/list-items/QasListItems.yml +71 -71
  127. package/src/components/list-view/QasListView.vue +302 -302
  128. package/src/components/list-view/QasListView.yml +169 -169
  129. package/src/components/map/QasMap.vue +75 -75
  130. package/src/components/map/QasMap.yml +33 -33
  131. package/src/components/nested-fields/QasNestedFields.vue +584 -584
  132. package/src/components/nested-fields/QasNestedFields.yml +296 -296
  133. package/src/components/numeric-input/QasNumericInput.vue +206 -206
  134. package/src/components/numeric-input/QasNumericInput.yml +85 -85
  135. package/src/components/option-group/QasOptionGroup.vue +54 -54
  136. package/src/components/option-group/QasOptionGroup.yml +30 -30
  137. package/src/components/page-header/QasPageHeader.vue +143 -143
  138. package/src/components/page-header/QasPageHeader.yml +42 -42
  139. package/src/components/pagination/QasPagination.vue +71 -71
  140. package/src/components/pagination/QasPagination.yml +4 -4
  141. package/src/components/password-input/QasPasswordInput.vue +110 -110
  142. package/src/components/password-input/QasPasswordInput.yml +111 -111
  143. package/src/components/password-strength-checker/QasPasswordStrengthChecker.vue +71 -71
  144. package/src/components/password-strength-checker/QasPasswordStrengthChecker.yml +103 -103
  145. package/src/components/profile/QasProfile.vue +98 -98
  146. package/src/components/profile/QasProfile.yml +60 -60
  147. package/src/components/radio/QasRadio.vue +62 -62
  148. package/src/components/radio/QasRadio.yml +11 -11
  149. package/src/components/resizer/QasResizer.vue +90 -90
  150. package/src/components/resizer/QasResizer.yml +24 -24
  151. package/src/components/search-box/QasSearchBox.vue +294 -294
  152. package/src/components/search-box/QasSearchBox.yml +154 -154
  153. package/src/components/search-input/QasSearchInput.vue +106 -106
  154. package/src/components/search-input/QasSearchInput.yml +48 -48
  155. package/src/components/select/QasSelect.vue +514 -514
  156. package/src/components/select/QasSelect.yml +157 -157
  157. package/src/components/select-list/QasSelectList.vue +238 -238
  158. package/src/components/select-list/QasSelectList.yml +108 -108
  159. package/src/components/select-list/private/PvSelectListCheckbox.vue +31 -31
  160. package/src/components/select-list-dialog/QasSelectListDialog.vue +395 -395
  161. package/src/components/select-list-dialog/QasSelectListDialog.yml +108 -108
  162. package/src/components/signature-pad/QasSignaturePad.vue +122 -122
  163. package/src/components/signature-pad/QasSignaturePad.yml +53 -53
  164. package/src/components/signature-uploader/QasSignatureUploader.vue +164 -164
  165. package/src/components/signature-uploader/QasSignatureUploader.yml +37 -37
  166. package/src/components/single-view/QasSingleView.vue +175 -175
  167. package/src/components/single-view/QasSingleView.yml +131 -131
  168. package/src/components/sortable/QasSortable.vue +151 -151
  169. package/src/components/sortable/QasSortable.yml +65 -65
  170. package/src/components/status/QasStatus.test.js +11 -11
  171. package/src/components/status/QasStatus.vue +29 -29
  172. package/src/components/status/QasStatus.yml +10 -10
  173. package/src/components/stepper/QasStepper.vue +152 -152
  174. package/src/components/stepper/QasStepper.yml +35 -35
  175. package/src/components/stepper-form-view/QasStepperFormView.vue +84 -84
  176. package/src/components/stepper-form-view/QasStepperFormView.yml +58 -58
  177. package/src/components/table-generator/QasTableGenerator.vue +416 -416
  178. package/src/components/table-generator/QasTableGenerator.yml +94 -94
  179. package/src/components/tabs-generator/QasTabsGenerator.vue +196 -196
  180. package/src/components/tabs-generator/QasTabsGenerator.yml +53 -53
  181. package/src/components/text-truncate/QasTextTruncate.vue +274 -274
  182. package/src/components/text-truncate/QasTextTruncate.yml +60 -60
  183. package/src/components/timeline/QasTimeline.vue +140 -140
  184. package/src/components/timeline/QasTimeline.yml +36 -36
  185. package/src/components/toggle/QasToggle.vue +14 -14
  186. package/src/components/toggle/QasToggle.yml +5 -5
  187. package/src/components/toggle-visibility/QasToggleVisibility.vue +79 -79
  188. package/src/components/toggle-visibility/QasToggleVisibility.yml +30 -30
  189. package/src/components/transfer/QasTransfer.vue +239 -239
  190. package/src/components/transfer/QasTransfer.yml +60 -60
  191. package/src/components/tree-generator/QasTreeForm.vue +62 -62
  192. package/src/components/tree-generator/QasTreeGenerator.vue +488 -488
  193. package/src/components/tree-generator/QasTreeGenerator.yml +81 -81
  194. package/src/components/uploader/QasUploader.vue +645 -645
  195. package/src/components/uploader/QasUploader.yml +192 -192
  196. package/src/components/uploader/private/PvUploaderGalleryCard.vue +357 -357
  197. package/src/components/welcome/QasWelcome.vue +125 -125
  198. package/src/components/welcome/QasWelcome.yml +23 -23
  199. package/src/components/welcome/private/PvWelcomeShortcutCard.vue +58 -58
  200. package/src/components/whatsapp-link/QasWhatsappLink.vue +34 -34
  201. package/src/components/whatsapp-link/QasWhatsappLink.yml +18 -18
  202. package/src/composables/index.js +8 -8
  203. package/src/composables/private/index.js +3 -3
  204. package/src/composables/private/use-generator.js +174 -174
  205. package/src/composables/private/use-toggle-visibility.js +48 -48
  206. package/src/composables/private/use-view.js +186 -186
  207. package/src/composables/use-context.js +15 -15
  208. package/src/composables/use-form.js +32 -32
  209. package/src/composables/use-history.js +46 -46
  210. package/src/composables/use-notifications.js +128 -114
  211. package/src/composables/use-query-cache.js +60 -60
  212. package/src/composables/use-screen.js +42 -42
  213. package/src/css/base/font-face.scss +28 -28
  214. package/src/css/base/index.scss +1 -1
  215. package/src/css/components/base.scss +12 -12
  216. package/src/css/components/button.scss +116 -116
  217. package/src/css/components/checkbox.scss +14 -14
  218. package/src/css/components/editor.scss +7 -7
  219. package/src/css/components/field.scss +88 -88
  220. package/src/css/components/index.scss +10 -10
  221. package/src/css/components/item.scss +52 -52
  222. package/src/css/components/radio.scss +18 -18
  223. package/src/css/components/scrollbar.scss +20 -20
  224. package/src/css/components/tabs.scss +3 -3
  225. package/src/css/components/toggle.scss +13 -13
  226. package/src/css/mixins/index.scss +3 -3
  227. package/src/css/mixins/set-brand.scss +15 -15
  228. package/src/css/mixins/set-button.scss +26 -26
  229. package/src/css/mixins/set-typography.scss +8 -8
  230. package/src/css/plugins/index.scss +2 -2
  231. package/src/css/plugins/loading.scss +5 -5
  232. package/src/css/plugins/notify.scss +100 -100
  233. package/src/css/utils/background.scss +34 -34
  234. package/src/css/utils/border-radius.scss +29 -29
  235. package/src/css/utils/border.scss +42 -42
  236. package/src/css/utils/container.scss +36 -36
  237. package/src/css/utils/fonts.scss +5 -5
  238. package/src/css/utils/index.scss +10 -10
  239. package/src/css/utils/line-height.scss +19 -19
  240. package/src/css/utils/opacity.scss +5 -5
  241. package/src/css/utils/scroll.scss +21 -21
  242. package/src/css/utils/text.scss +15 -15
  243. package/src/css/utils/unset.scss +8 -8
  244. package/src/css/variables/button.scss +4 -4
  245. package/src/css/variables/index.scss +6 -6
  246. package/src/css/variables/scrollbar.scss +11 -11
  247. package/src/css/variables/separator.scss +2 -2
  248. package/src/css/variables/shadow.scss +3 -3
  249. package/src/css/variables/spacing.scss +98 -98
  250. package/src/css/variables/typography.scss +149 -149
  251. package/src/directives/Test.js +13 -13
  252. package/src/enums/Align.js +7 -7
  253. package/src/enums/Spacing.js +98 -98
  254. package/src/enums/Status.js +33 -33
  255. package/src/helpers/add-counter-suffix.js +3 -3
  256. package/src/helpers/base-64-to-blob.js +21 -21
  257. package/src/helpers/camelize-fields-name.js +15 -15
  258. package/src/helpers/construct-object.js +29 -29
  259. package/src/helpers/copy-to-clipboard.js +15 -15
  260. package/src/helpers/destroy-nested-children-by-key.js +33 -33
  261. package/src/helpers/download-file.js +21 -21
  262. package/src/helpers/filter-list-by-handle.js +31 -31
  263. package/src/helpers/filter-object-to-array.js +29 -29
  264. package/src/helpers/filter-object.js +34 -34
  265. package/src/helpers/filters.js +163 -163
  266. package/src/helpers/find-children-by-key.js +14 -14
  267. package/src/helpers/get-greatest-common-divisor.js +16 -16
  268. package/src/helpers/get-normalized-options.js +20 -20
  269. package/src/helpers/get-placeholder.js +19 -19
  270. package/src/helpers/get-required-label.js +3 -3
  271. package/src/helpers/get-slot-children-text.js +15 -15
  272. package/src/helpers/handle-process.js +13 -13
  273. package/src/helpers/images.js +28 -28
  274. package/src/helpers/index.js +25 -25
  275. package/src/helpers/is-empty.js +3 -3
  276. package/src/helpers/is-local-development.js +3 -3
  277. package/src/helpers/private/gutter-validator.js +15 -15
  278. package/src/helpers/private/has-parent-by-class-name.js +15 -15
  279. package/src/helpers/promise-handler.js +40 -40
  280. package/src/helpers/rules.js +7 -7
  281. package/src/helpers/set-scroll-on-grab.js +128 -128
  282. package/src/index.cjs.js +1 -1
  283. package/src/index.esm.js +4 -4
  284. package/src/index.scss +38 -38
  285. package/src/index.umd.js +2 -2
  286. package/src/mixins/context.js +8 -8
  287. package/src/mixins/delete.js +48 -48
  288. package/src/mixins/dialog-router.js +17 -17
  289. package/src/mixins/form.js +11 -11
  290. package/src/mixins/generator.js +94 -94
  291. package/src/mixins/index.js +17 -17
  292. package/src/mixins/password.js +86 -86
  293. package/src/mixins/search-filter.js +354 -354
  294. package/src/mixins/view.js +156 -156
  295. package/src/pages/ErrorComponent.vue +74 -74
  296. package/src/pages/Forbidden.vue +29 -29
  297. package/src/pages/NotFound.vue +29 -29
  298. package/src/pages/ServerError.vue +29 -29
  299. package/src/pages/Unauthorized.vue +29 -29
  300. package/src/plugins/delete/Delete.js +107 -107
  301. package/src/plugins/delete/Delete.yml +75 -75
  302. package/src/plugins/dialog/Dialog.js +14 -14
  303. package/src/plugins/dialog/Dialog.yml +10 -10
  304. package/src/plugins/index.js +13 -13
  305. package/src/plugins/notify-error/NotifyError.js +13 -13
  306. package/src/plugins/notify-error/NotifyError.yml +11 -11
  307. package/src/plugins/notify-success/NotifySuccess.js +13 -13
  308. package/src/plugins/notify-success/NotifySuccess.yml +11 -11
  309. package/src/plugins/screen/Screen.js +35 -35
  310. package/src/plugins/screen/Screen.yml +16 -16
  311. package/src/shared/date-config.js +26 -26
  312. package/src/shared/fuse-config.js +4 -4
  313. package/src/shared/notify-config.js +7 -7
  314. package/src/vue-plugin/third-party-component-handler.js +29 -29
  315. package/src/vue-plugin.js +282 -282
@@ -1,712 +1,712 @@
1
- <template>
2
- <qas-grabbable class="qas-board-generator" v-bind="grabbableProps">
3
- <div class="no-wrap q-col-gutter-sm q-px-xl row">
4
- <div v-for="(header, index) in headers" :key="index" class="q-mr-sm">
5
- <qas-box class="q-mb-md" v-bind="headerBoxProps">
6
- <slot :fields="getFieldsByHeader(header)" :header="header" :index="index" name="header-column" />
7
- </qas-box>
8
-
9
- <div ref="columnContainer" class="qas-board-generator__column secondary-scroll" :data-header-key="getKeyByHeader(header)" :style="containerStyle">
10
- <div v-for="item in getItemsByHeader(header)" :id="item[props.itemIdKey]" :key="item[props.itemIdKey]" class="qas-board-generator__item">
11
- <slot :column-index="index" :fields="getFieldsByHeader(header)" :item="item" name="column-item" />
12
- </div>
13
-
14
- <div class="full-width justify-center row">
15
- <qas-btn v-if="hasSeeMore(header)" icon="sym_r_add" label="Ver mais" :use-label-on-small-screen="false" variant="tertiary" @click="fetchColumn(header)" />
16
-
17
- <q-spinner v-if="columnsLoading[getKeyByHeader(header)]" class="q-mb-md" color="grey-4" size="3em" />
18
- </div>
19
-
20
- <qas-empty-result-text v-if="hasEmptyResultText(header)" />
21
- </div>
22
- </div>
23
- </div>
24
-
25
- <qas-dialog v-model="showConfirmDialog" v-bind="defaultConfirmDialogProps" />
26
- </qas-grabbable>
27
- </template>
28
-
29
- <script setup>
30
- import QasDialog from '../dialog/QasDialog.vue'
31
-
32
- import { ref, watch, computed, onUnmounted, markRaw, inject, onMounted } from 'vue'
33
- import promiseHandler from '../../helpers/promise-handler'
34
-
35
- import Sortable from 'sortablejs'
36
-
37
- defineOptions({ name: 'QasBoardGenerator' })
38
-
39
- const props = defineProps({
40
- headers: {
41
- type: Array,
42
- default: () => []
43
- },
44
-
45
- results: {
46
- type: Object,
47
- default: () => ({})
48
- },
49
-
50
- headerBoxProps: {
51
- type: Object,
52
- default: () => ({})
53
- },
54
-
55
- columnIdKey: {
56
- type: String,
57
- required: true
58
- },
59
-
60
- columnParams: {
61
- type: Object,
62
- default: () => ({})
63
- },
64
-
65
- columnUrl: {
66
- type: String,
67
- required: true
68
- },
69
-
70
- confirmDialogProps: {
71
- type: Object,
72
- default: () => ({})
73
- },
74
-
75
- height: {
76
- type: String,
77
- default: ''
78
- },
79
-
80
- itemIdKey: {
81
- type: String,
82
- default: ''
83
- },
84
-
85
- limitPerColumn: {
86
- type: Number,
87
- default: 12
88
- },
89
-
90
- columnWidth: {
91
- type: String,
92
- default: '300px'
93
- },
94
-
95
- sortableConfig: {
96
- type: Object,
97
- default: () => ({})
98
- },
99
-
100
- useMarkRaw: {
101
- type: Boolean,
102
- default: true
103
- },
104
-
105
- useDragAndDropX: {
106
- type: Boolean
107
- },
108
-
109
- useDragAndDropY: {
110
- type: Boolean
111
- },
112
-
113
- updatePositionUrl: {
114
- type: String,
115
- default: ''
116
- },
117
-
118
- updatePositionParams: {
119
- type: Object,
120
- default: () => ({})
121
- },
122
-
123
- lazyLoadingFieldsKeys: {
124
- type: Array,
125
- default: () => []
126
- }
127
- })
128
-
129
- const emit = defineEmits([
130
- 'update:results',
131
- 'fetch-column-success',
132
- 'fetch-column-error',
133
- 'fetch-columns-success',
134
- 'fetch-columns-error',
135
- 'update-success'
136
- ])
137
-
138
- defineExpose({ fetchColumns, fetchColumn, reset })
139
-
140
- // Inject
141
- const axios = inject('axios')
142
-
143
- const isFetchSuccessHeader = inject('isFetchListSucceeded', false)
144
-
145
- const isInsideListView = inject('isListView', false)
146
-
147
- // Refs
148
- const columnContainer = ref(null)
149
- const columnsPagination = ref({})
150
- const columnsLoading = ref({})
151
- const columnsFieldsModel = ref({})
152
- const showConfirmDialog = ref(false)
153
- const isDragging = ref(false)
154
- const isLoadingUpdatePosition = ref(false)
155
-
156
- /**
157
- * Instâncias do sortable, que são utilizadas para realizar o destroy ao sair da página
158
- */
159
- const sortableInstances = ref([])
160
-
161
- /**
162
- * Callbacks que recebe o event de movimentação
163
- */
164
- const onCancelDrop = ref(() => {})
165
- const onConfirmDrop = ref(() => {})
166
-
167
- /**
168
- * Variável auxiliar que controla quando estou atualizando o header em caso de drag and drop
169
- */
170
- const isUpdatingPosition = ref(false)
171
-
172
- // Consts
173
- const hasDragAndDrop = !!props.useDragAndDropX || !!props.useDragAndDropY
174
-
175
- const grabbableProps = {
176
- useScrollBar: true,
177
-
178
- ...(hasDragAndDrop && {
179
- cancelMouseDownTarget: 'qas-board-generator__item'
180
- })
181
- }
182
-
183
- // Watchers
184
- watch(
185
- () => isFetchSuccessHeader.value,
186
- value => {
187
- /**
188
- * isFetchSuccessHeader é uma variavel que pego do listView por inject/provide, no qual caso eu faça request do header e dê sucesso, eu chamo as demais funções.
189
- * Valido se não houve sucesso na requisição do header ou se não é uma atualização de posição, para assim não bater novamente nas colunas apenas no header.
190
- */
191
- if (!value || isUpdatingPosition.value) return
192
-
193
- fetchColumnsValues()
194
- }
195
- )
196
-
197
- watch(
198
- () => props.headers,
199
- () => {
200
- if (isUpdatingPosition.value) return
201
-
202
- isUpdatingPosition.value = false
203
- }
204
- )
205
-
206
- watch(columnContainer, setColumnHeightContainer)
207
-
208
- // Lifecycles
209
- onMounted(() => {
210
- /**
211
- * Caso eu use o listView (valor pego por provide), a request é feito pelo watch quando se ocorre o sucesso do `fetchList`
212
- */
213
- if (isInsideListView) return
214
-
215
- fetchColumnsValues()
216
- })
217
-
218
- onUnmounted(destroySortable)
219
-
220
- // Computeds
221
- const columnsResultsModel = computed({
222
- get () {
223
- return props.results
224
- },
225
-
226
- set (newValues) {
227
- emit('update:results', newValues)
228
- }
229
- })
230
-
231
- const hasColumnsLength = computed(() => !!Object.keys(columnsResultsModel.value).length)
232
-
233
- const containerStyle = computed(() => `width: ${props.columnWidth};`)
234
-
235
- const hasConfirmDialogProps = computed(() => !!Object.keys(props.confirmDialogProps).length)
236
-
237
- const defaultConfirmDialogProps = computed(() => {
238
- const defaultProps = {
239
- ok: {
240
- label: 'Confirmar',
241
- onClick: onConfirmDrop.value,
242
- loading: isLoadingUpdatePosition.value
243
- },
244
-
245
- cancel: {
246
- onClick: onCancelDrop.value
247
- }
248
- }
249
-
250
- return {
251
- ...props.confirmDialogProps,
252
- ...defaultProps
253
- }
254
- })
255
-
256
- // functions
257
- /*
258
- * Setar o tamanho do container do board, onde deverá ser a altura passada via prop, ou o default será ocupar o maximo
259
- * de espaço que ele conseguir considerando a altura do container em relação ao topo.
260
- */
261
- function setColumnHeightContainer () {
262
- columnContainer.value?.forEach(columnElement => {
263
- const heightToTop = columnElement?.getBoundingClientRect()?.top
264
- const paddingSpacing = 60
265
- const value = heightToTop + paddingSpacing
266
-
267
- columnElement.style.setProperty('height', props.height ? props.height : `calc(100vh - ${value}px)`)
268
- })
269
- }
270
-
271
- /*
272
- * Bater API pra cada header
273
- */
274
- async function fetchColumns () {
275
- const promises = props.headers.map(header => fetchColumn(header))
276
-
277
- const { error } = await promiseHandler(promises, { useLoading: false })
278
-
279
- if (error) {
280
- emit('fetch-columns-error', error)
281
-
282
- return
283
- }
284
-
285
- emit('fetch-columns-success')
286
-
287
- if (hasDragAndDrop) handleElementsList()
288
- }
289
-
290
- /*
291
- * Busca a coluna com base no header recebido.
292
- */
293
- async function fetchColumn (header) {
294
- const headerKey = getKeyByHeader(header)
295
- const { limit, offset } = columnsPagination.value[headerKey] || {}
296
-
297
- const { data: response, error } = await promiseHandler(
298
- axios.get(`${props.columnUrl}/${headerKey}`, {
299
- params: {
300
- ...props.columnParams,
301
- limit,
302
- offset
303
- }
304
- }),
305
- {
306
- onLoading: value => {
307
- columnsLoading.value[headerKey] = value
308
- },
309
- useLoading: false,
310
- errorMessage: 'Não conseguimos buscar as colunas do board. Por favor, tente novamente em alguns minutos.'
311
- }
312
- )
313
-
314
- if (error) {
315
- emit('fetch-column-error', error)
316
-
317
- throw error
318
- }
319
-
320
- const newValues = response.data?.results || []
321
- const resultsModel = columnsResultsModel.value[headerKey] || []
322
-
323
- const newColumnValues = [
324
- ...resultsModel,
325
- ...newValues
326
- ]
327
-
328
- /**
329
- * exemplo de como columnsResultsModel irá ficar:
330
- *
331
- * {
332
- * '2024-02-15': [...],
333
- * '2024-02-16': [...]
334
- * }
335
- *
336
- * onde cada item do objeto é uma coluna no board. O mesmo vale para "columnsFieldsModel", "columnsLoading" e
337
- * "columnPagination", organizando os fields, loadings e o controle de paginação por chave identificadora do header.
338
- */
339
- columnsResultsModel.value[headerKey] = props.useMarkRaw ? markRaw(newColumnValues) : newColumnValues
340
-
341
- /*
342
- * Pode acontecer das options nos fields da segunda página serem diferentes da primeira página,
343
- * portanto deve ocorrer o merge.
344
- */
345
- if (response.data?.fields) {
346
- columnsFieldsModel.value[headerKey] = markRaw(
347
- getMergedFields(columnsFieldsModel.value[headerKey], response.data?.fields)
348
- )
349
- }
350
-
351
- columnsPagination.value[headerKey].offset = columnsResultsModel.value[headerKey].length
352
- columnsPagination.value[headerKey].count = response.data?.count
353
-
354
- emit('fetch-column-success', { response, header })
355
- }
356
-
357
- /*
358
- * Mergeia os options antigos com os novos de cada field.
359
- */
360
- function getMergedFields (oldFields, newFields) {
361
- // Primeira vez batendo a API, retorna os novos fields.
362
- if (!oldFields || !props.lazyLoadingFieldsKeys.length) return newFields
363
-
364
- // Caso bata a API e por algum motivo não venha fields, mantenha o antigos.
365
- if (oldFields && !newFields) return oldFields
366
-
367
- const mergedFields = { ...oldFields }
368
-
369
- props.lazyLoadingFieldsKeys.forEach(fieldKey => {
370
- mergedFields[fieldKey].options = getNonDuplicatedOptions(oldFields[fieldKey].options, newFields[fieldKey].options)
371
- })
372
-
373
- return mergedFields
374
- }
375
-
376
- /*
377
- * Tratamento para fazer o merge e evitar options duplicados.
378
- */
379
- function getNonDuplicatedOptions (oldOptions, newOptions) {
380
- const options = [...oldOptions]
381
-
382
- newOptions.forEach(item => {
383
- const hasOption = options.find(option => option.value === item.value)
384
-
385
- if (!hasOption) options.push(item)
386
- })
387
-
388
- return options
389
- }
390
-
391
- function getItemsByHeader (header) {
392
- return hasColumnsLength.value ? columnsResultsModel.value[getKeyByHeader(header)] : []
393
- }
394
-
395
- /**
396
- * Pegar key com base na chave identificador, exemplo:
397
- * header -> { date: '2024-02-12', ... }
398
- * columnIdKey -> 'date'
399
- * retorno -> '2024-02-12'
400
- *
401
- * Onde esta chave será o "id" da coluna, sendo usado para bater a API, lidar com paginação, loading, etc.
402
- *
403
- * @example getKeyByHeader({ date: '2024-02-12', ... })
404
- * @returns {string} // '2024-02-12'
405
- */
406
- function getKeyByHeader (header = {}) {
407
- return header[props.columnIdKey]
408
- }
409
-
410
- /*
411
- * Para cada header, irá ser criado um item com sua chave identificadora para lidar com paginação e loading.
412
- * columnsPagination -> { '2024-02-15': { limit: 12, offset: 0 }, '2024-02-16': { limit: 12, offset: 0 }, ... }
413
- * columnsLoading ->{ '2024-02-15': false, '2024-02-16': false, ... }
414
- */
415
- function setColumnsPagination () {
416
- columnsPagination.value = {}
417
- columnsLoading.value = {}
418
-
419
- props.headers.forEach(header => {
420
- const headerKey = getKeyByHeader(header)
421
-
422
- columnsPagination.value[headerKey] = { limit: props.limitPerColumn, offset: 0 }
423
- columnsLoading.value[headerKey] = false
424
- })
425
- }
426
-
427
- function fetchColumnsValues () {
428
- reset()
429
- setColumnHeightContainer()
430
- setColumnsPagination()
431
- fetchColumns()
432
- }
433
-
434
- /**
435
- * Descricao:
436
- * Exibe o texto quando:
437
- * - Nao esta carregando a coluna
438
- * - Nao tem itens na coluna
439
- * - Nao estou fazendo o drag and drop
440
- *
441
- * @param {Object} header
442
- */
443
- function hasEmptyResultText (header) {
444
- return !columnsLoading.value[getKeyByHeader(header)] && !getItemsByHeader(header)?.length && !isDragging.value
445
- }
446
-
447
- /*
448
- * Valida se o tamanho dos itens da coluna é menor que o valor total de itens que o back retorna e
449
- * se a coluna não está em carregamento.
450
- */
451
- function hasSeeMore (header) {
452
- const headerKey = getKeyByHeader(header)
453
- const hasMorePagination = columnsResultsModel.value[headerKey]?.length < columnsPagination.value[headerKey]?.count
454
-
455
- return hasMorePagination && !columnsLoading.value[headerKey]
456
- }
457
-
458
- function reset () {
459
- columnsResultsModel.value = {}
460
- columnsPagination.value = {}
461
- columnsLoading.value = {}
462
- }
463
-
464
- function getFieldsByHeader (header) {
465
- const headerKey = getKeyByHeader(header)
466
-
467
- return columnsFieldsModel.value[headerKey] || {}
468
- }
469
-
470
- /**
471
- * Loopa todos os itens da coluna com base no ref para pegar o elemento HTML e setar e instaciar o sortable.
472
- */
473
- function handleElementsList () {
474
- columnContainer.value.forEach((element, index) => {
475
- const sortable = setSortable(element, index)
476
-
477
- sortableInstances.value.push(sortable)
478
- })
479
- }
480
-
481
- /**
482
- * Descricao:
483
- * Seta a instancia do sortable, no qual varia de acordo com as props passadas.
484
- *
485
- * @param {HTMLElement} element
486
- * @param {Number} index
487
- */
488
- function setSortable (element, index) {
489
- const defaultSortableConfig = {
490
- animation: 500,
491
- swapThreshold: 1,
492
- delay: 50,
493
- delayOnTouchOnly: true,
494
- emptyInsertThreshold: 0
495
- }
496
-
497
- /**
498
- * Caso seja apenas drag and drop no eixo Y
499
- */
500
- const useOnlyDragAndDropY = !!props.useDragAndDropY && !props.useDragAndDropX
501
-
502
- const sortable = new Sortable(element, {
503
- sort: props.useDragAndDropY,
504
-
505
- ...defaultSortableConfig,
506
-
507
- ...props.sortableConfig,
508
-
509
- group: useOnlyDragAndDropY ? `column-${index}` : 'shared',
510
-
511
- direction: useOnlyDragAndDropY ? 'vertical' : 'horizontal',
512
-
513
- onStart: toggleIsDragging,
514
-
515
- onAdd: event => onDropCard(event),
516
-
517
- ...(props.useDragAndDropY && {
518
- onSort: event => onDropCard(event)
519
- })
520
- })
521
-
522
- return sortable
523
- }
524
-
525
- function toggleIsDragging () {
526
- isDragging.value = !isDragging.value
527
- }
528
-
529
- function onDropCard (event) {
530
- onCancelDrop.value = () => cancelDrop(event)
531
-
532
- onConfirmDrop.value = () => confirmDrop(event)
533
-
534
- hasConfirmDialogProps.value
535
- ? openConfirmDialog()
536
- : confirmDrop(event)
537
- }
538
-
539
- function openConfirmDialog () {
540
- showConfirmDialog.value = true
541
- }
542
-
543
- function closeConfirmDialog () {
544
- showConfirmDialog.value = false
545
- }
546
-
547
- /**
548
- * @param {event} event
549
- */
550
- function cancelDrop (event) {
551
- /**
552
- * Insere na posição antiga que pertencia (event.oldIndex) dentro do seu antigo pai (event.from)
553
- */
554
- if (props.useDragAndDropX) event.from.insertBefore(event.item, event.from.children[event.oldIndex])
555
-
556
- if (props.useDragAndDropY) {
557
- const oldIndex = event.oldIndex
558
-
559
- /**
560
- * Se oldIndex for 0, o targetIndex deverá ser 0, pois isso indica que se o item é o primeiro da lista, ele não será movido para outra posição.
561
- *
562
- * Caso o oldIndex for diferente, devo incrementar 1 para adicionar, pois isso permite que o item seja inserido logo após sua posição original.
563
- */
564
- const targetIndex = oldIndex === 0 ? oldIndex : oldIndex + 1
565
-
566
- /**
567
- * Verifica se o índice alvo é válido, caso contrário, define como o final
568
- */
569
- const insertBeforeElement = targetIndex < event.from.children.length
570
- ? event.from.children[targetIndex]
571
- : null
572
-
573
- event.from.insertBefore(event.item, insertBeforeElement)
574
- }
575
-
576
- if (hasConfirmDialogProps.value) closeConfirmDialog()
577
-
578
- toggleIsDragging()
579
- }
580
-
581
- function confirmDrop (event) {
582
- const { from, to, item: { id: itemId } } = event
583
-
584
- const { headerKey: newHeaderKey } = to.dataset
585
- const { headerKey: oldHeaderKey } = from.dataset
586
-
587
- updatePosition({ newHeaderKey, oldHeaderKey, itemId, event })
588
- }
589
-
590
- /**
591
- *
592
- * @param {{
593
- * headerKey: string,
594
- * itemId: string
595
- * }}
596
- */
597
- function removeItemFromList ({ headerKey, itemId }) {
598
- /**
599
- * Coluna referente ao model de resultado
600
- */
601
- const columnItemList = columnsResultsModel.value[headerKey]
602
-
603
- /**
604
- * Busca o item com base em seu ID na lista de itens da coluna
605
- */
606
- const itemIndex = columnItemList.findIndex(itemContent => itemContent[props.itemIdKey] === itemId)
607
-
608
- /**
609
- * Remove o item da listagem com base no index, sendo que preciso subtrair 1 para pegar o index correto
610
- */
611
- columnItemList.splice(itemIndex, 1)
612
-
613
- /**
614
- * Remove o item do count da coluna para não mostrar o botão de "Ver mais¨.
615
- */
616
- columnsPagination.value[headerKey].count -= 1
617
- }
618
-
619
- /**
620
- * Descricao:
621
- * Metodo que realiza a request de update
622
- *
623
- * @param {{
624
- * newHeaderKey: string - ID da coluna de destino,
625
- * oldHeaderKey: string - ID da antiga coluna,
626
- * itemId: string - ID do meu item a ser movimento,
627
- * event: event
628
- * }}
629
- */
630
- async function updatePosition ({ newHeaderKey, oldHeaderKey, itemId, event }) {
631
- const params = {
632
- [props.columnIdKey]: newHeaderKey,
633
- ...(props.useDragAndDropY && { newIndex: event.newIndex }),
634
- ...props.updatePositionParams
635
- }
636
-
637
- const { data, error } = await promiseHandler(
638
- axios.patch(`${props.updatePositionUrl}/${itemId}/update-position`, params),
639
- {
640
- errorMessage: 'Ocorreu um erro ao atualizar a posição de seu item.',
641
- useLoading: false,
642
- onLoading: value => {
643
- isLoadingUpdatePosition.value = value
644
-
645
- columnsLoading.value[newHeaderKey] = value
646
- }
647
- }
648
- )
649
-
650
- if (error) {
651
- onCancelDrop.value()
652
-
653
- return
654
- }
655
-
656
- removeItemFromList({ headerKey: oldHeaderKey, itemId })
657
-
658
- setItemList({ headerKey: newHeaderKey, data: data.data, index: event.newIndex })
659
-
660
- isUpdatingPosition.value = true
661
-
662
- toggleIsDragging()
663
-
664
- closeConfirmDialog()
665
-
666
- emit('update-success')
667
- }
668
-
669
- function setItemList ({ headerKey, data, index }) {
670
- /**
671
- * Coluna referente ao model de resultado
672
- */
673
- const columnItemList = columnsResultsModel.value[headerKey]
674
-
675
- /**
676
- * Adiciona o item na posição do event escolhido.
677
- */
678
- columnItemList.splice(index, 0, data.result)
679
- }
680
-
681
- function destroySortable () {
682
- sortableInstances.value.forEach(sortable => sortable.destroy())
683
- }
684
- </script>
685
-
686
- <style lang="scss">
687
- .qas-board-generator {
688
- max-height: 100vh;
689
-
690
- &__column {
691
- overflow-x: hidden;
692
- scrollbar-width: none;
693
-
694
- &:hover {
695
- scrollbar-width: thin;
696
-
697
- &::-webkit-scrollbar {
698
- display: block;
699
- }
700
- }
701
-
702
- &::-webkit-scrollbar {
703
- display: none;
704
- }
705
- }
706
-
707
- // 60px é o valor do padding definido no container da column.
708
- &__column-items {
709
- height: calc(100% - 60px);
710
- }
711
- }
712
- </style>
1
+ <template>
2
+ <qas-grabbable class="qas-board-generator" v-bind="grabbableProps">
3
+ <div class="no-wrap q-col-gutter-sm q-px-xl row">
4
+ <div v-for="(header, index) in headers" :key="index" class="q-mr-sm">
5
+ <qas-box class="q-mb-md" v-bind="headerBoxProps">
6
+ <slot :fields="getFieldsByHeader(header)" :header="header" :index="index" name="header-column" />
7
+ </qas-box>
8
+
9
+ <div ref="columnContainer" class="qas-board-generator__column secondary-scroll" :data-header-key="getKeyByHeader(header)" :style="containerStyle">
10
+ <div v-for="item in getItemsByHeader(header)" :id="item[props.itemIdKey]" :key="item[props.itemIdKey]" class="qas-board-generator__item">
11
+ <slot :column-index="index" :fields="getFieldsByHeader(header)" :item="item" name="column-item" />
12
+ </div>
13
+
14
+ <div class="full-width justify-center row">
15
+ <qas-btn v-if="hasSeeMore(header)" icon="sym_r_add" label="Ver mais" :use-label-on-small-screen="false" variant="tertiary" @click="fetchColumn(header)" />
16
+
17
+ <q-spinner v-if="columnsLoading[getKeyByHeader(header)]" class="q-mb-md" color="grey-4" size="3em" />
18
+ </div>
19
+
20
+ <qas-empty-result-text v-if="hasEmptyResultText(header)" />
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <qas-dialog v-model="showConfirmDialog" v-bind="defaultConfirmDialogProps" />
26
+ </qas-grabbable>
27
+ </template>
28
+
29
+ <script setup>
30
+ import QasDialog from '../dialog/QasDialog.vue'
31
+
32
+ import { ref, watch, computed, onUnmounted, markRaw, inject, onMounted } from 'vue'
33
+ import promiseHandler from '../../helpers/promise-handler'
34
+
35
+ import Sortable from 'sortablejs'
36
+
37
+ defineOptions({ name: 'QasBoardGenerator' })
38
+
39
+ const props = defineProps({
40
+ headers: {
41
+ type: Array,
42
+ default: () => []
43
+ },
44
+
45
+ results: {
46
+ type: Object,
47
+ default: () => ({})
48
+ },
49
+
50
+ headerBoxProps: {
51
+ type: Object,
52
+ default: () => ({})
53
+ },
54
+
55
+ columnIdKey: {
56
+ type: String,
57
+ required: true
58
+ },
59
+
60
+ columnParams: {
61
+ type: Object,
62
+ default: () => ({})
63
+ },
64
+
65
+ columnUrl: {
66
+ type: String,
67
+ required: true
68
+ },
69
+
70
+ confirmDialogProps: {
71
+ type: Object,
72
+ default: () => ({})
73
+ },
74
+
75
+ height: {
76
+ type: String,
77
+ default: ''
78
+ },
79
+
80
+ itemIdKey: {
81
+ type: String,
82
+ default: ''
83
+ },
84
+
85
+ limitPerColumn: {
86
+ type: Number,
87
+ default: 12
88
+ },
89
+
90
+ columnWidth: {
91
+ type: String,
92
+ default: '300px'
93
+ },
94
+
95
+ sortableConfig: {
96
+ type: Object,
97
+ default: () => ({})
98
+ },
99
+
100
+ useMarkRaw: {
101
+ type: Boolean,
102
+ default: true
103
+ },
104
+
105
+ useDragAndDropX: {
106
+ type: Boolean
107
+ },
108
+
109
+ useDragAndDropY: {
110
+ type: Boolean
111
+ },
112
+
113
+ updatePositionUrl: {
114
+ type: String,
115
+ default: ''
116
+ },
117
+
118
+ updatePositionParams: {
119
+ type: Object,
120
+ default: () => ({})
121
+ },
122
+
123
+ lazyLoadingFieldsKeys: {
124
+ type: Array,
125
+ default: () => []
126
+ }
127
+ })
128
+
129
+ const emit = defineEmits([
130
+ 'update:results',
131
+ 'fetch-column-success',
132
+ 'fetch-column-error',
133
+ 'fetch-columns-success',
134
+ 'fetch-columns-error',
135
+ 'update-success'
136
+ ])
137
+
138
+ defineExpose({ fetchColumns, fetchColumn, reset })
139
+
140
+ // Inject
141
+ const axios = inject('axios')
142
+
143
+ const isFetchSuccessHeader = inject('isFetchListSucceeded', false)
144
+
145
+ const isInsideListView = inject('isListView', false)
146
+
147
+ // Refs
148
+ const columnContainer = ref(null)
149
+ const columnsPagination = ref({})
150
+ const columnsLoading = ref({})
151
+ const columnsFieldsModel = ref({})
152
+ const showConfirmDialog = ref(false)
153
+ const isDragging = ref(false)
154
+ const isLoadingUpdatePosition = ref(false)
155
+
156
+ /**
157
+ * Instâncias do sortable, que são utilizadas para realizar o destroy ao sair da página
158
+ */
159
+ const sortableInstances = ref([])
160
+
161
+ /**
162
+ * Callbacks que recebe o event de movimentação
163
+ */
164
+ const onCancelDrop = ref(() => {})
165
+ const onConfirmDrop = ref(() => {})
166
+
167
+ /**
168
+ * Variável auxiliar que controla quando estou atualizando o header em caso de drag and drop
169
+ */
170
+ const isUpdatingPosition = ref(false)
171
+
172
+ // Consts
173
+ const hasDragAndDrop = !!props.useDragAndDropX || !!props.useDragAndDropY
174
+
175
+ const grabbableProps = {
176
+ useScrollBar: true,
177
+
178
+ ...(hasDragAndDrop && {
179
+ cancelMouseDownTarget: 'qas-board-generator__item'
180
+ })
181
+ }
182
+
183
+ // Watchers
184
+ watch(
185
+ () => isFetchSuccessHeader.value,
186
+ value => {
187
+ /**
188
+ * isFetchSuccessHeader é uma variavel que pego do listView por inject/provide, no qual caso eu faça request do header e dê sucesso, eu chamo as demais funções.
189
+ * Valido se não houve sucesso na requisição do header ou se não é uma atualização de posição, para assim não bater novamente nas colunas apenas no header.
190
+ */
191
+ if (!value || isUpdatingPosition.value) return
192
+
193
+ fetchColumnsValues()
194
+ }
195
+ )
196
+
197
+ watch(
198
+ () => props.headers,
199
+ () => {
200
+ if (isUpdatingPosition.value) return
201
+
202
+ isUpdatingPosition.value = false
203
+ }
204
+ )
205
+
206
+ watch(columnContainer, setColumnHeightContainer)
207
+
208
+ // Lifecycles
209
+ onMounted(() => {
210
+ /**
211
+ * Caso eu use o listView (valor pego por provide), a request é feito pelo watch quando se ocorre o sucesso do `fetchList`
212
+ */
213
+ if (isInsideListView) return
214
+
215
+ fetchColumnsValues()
216
+ })
217
+
218
+ onUnmounted(destroySortable)
219
+
220
+ // Computeds
221
+ const columnsResultsModel = computed({
222
+ get () {
223
+ return props.results
224
+ },
225
+
226
+ set (newValues) {
227
+ emit('update:results', newValues)
228
+ }
229
+ })
230
+
231
+ const hasColumnsLength = computed(() => !!Object.keys(columnsResultsModel.value).length)
232
+
233
+ const containerStyle = computed(() => `width: ${props.columnWidth};`)
234
+
235
+ const hasConfirmDialogProps = computed(() => !!Object.keys(props.confirmDialogProps).length)
236
+
237
+ const defaultConfirmDialogProps = computed(() => {
238
+ const defaultProps = {
239
+ ok: {
240
+ label: 'Confirmar',
241
+ onClick: onConfirmDrop.value,
242
+ loading: isLoadingUpdatePosition.value
243
+ },
244
+
245
+ cancel: {
246
+ onClick: onCancelDrop.value
247
+ }
248
+ }
249
+
250
+ return {
251
+ ...props.confirmDialogProps,
252
+ ...defaultProps
253
+ }
254
+ })
255
+
256
+ // functions
257
+ /*
258
+ * Setar o tamanho do container do board, onde deverá ser a altura passada via prop, ou o default será ocupar o maximo
259
+ * de espaço que ele conseguir considerando a altura do container em relação ao topo.
260
+ */
261
+ function setColumnHeightContainer () {
262
+ columnContainer.value?.forEach(columnElement => {
263
+ const heightToTop = columnElement?.getBoundingClientRect()?.top
264
+ const paddingSpacing = 60
265
+ const value = heightToTop + paddingSpacing
266
+
267
+ columnElement.style.setProperty('height', props.height ? props.height : `calc(100vh - ${value}px)`)
268
+ })
269
+ }
270
+
271
+ /*
272
+ * Bater API pra cada header
273
+ */
274
+ async function fetchColumns () {
275
+ const promises = props.headers.map(header => fetchColumn(header))
276
+
277
+ const { error } = await promiseHandler(promises, { useLoading: false })
278
+
279
+ if (error) {
280
+ emit('fetch-columns-error', error)
281
+
282
+ return
283
+ }
284
+
285
+ emit('fetch-columns-success')
286
+
287
+ if (hasDragAndDrop) handleElementsList()
288
+ }
289
+
290
+ /*
291
+ * Busca a coluna com base no header recebido.
292
+ */
293
+ async function fetchColumn (header) {
294
+ const headerKey = getKeyByHeader(header)
295
+ const { limit, offset } = columnsPagination.value[headerKey] || {}
296
+
297
+ const { data: response, error } = await promiseHandler(
298
+ axios.get(`${props.columnUrl}/${headerKey}`, {
299
+ params: {
300
+ ...props.columnParams,
301
+ limit,
302
+ offset
303
+ }
304
+ }),
305
+ {
306
+ onLoading: value => {
307
+ columnsLoading.value[headerKey] = value
308
+ },
309
+ useLoading: false,
310
+ errorMessage: 'Não conseguimos buscar as colunas do board. Por favor, tente novamente em alguns minutos.'
311
+ }
312
+ )
313
+
314
+ if (error) {
315
+ emit('fetch-column-error', error)
316
+
317
+ throw error
318
+ }
319
+
320
+ const newValues = response.data?.results || []
321
+ const resultsModel = columnsResultsModel.value[headerKey] || []
322
+
323
+ const newColumnValues = [
324
+ ...resultsModel,
325
+ ...newValues
326
+ ]
327
+
328
+ /**
329
+ * exemplo de como columnsResultsModel irá ficar:
330
+ *
331
+ * {
332
+ * '2024-02-15': [...],
333
+ * '2024-02-16': [...]
334
+ * }
335
+ *
336
+ * onde cada item do objeto é uma coluna no board. O mesmo vale para "columnsFieldsModel", "columnsLoading" e
337
+ * "columnPagination", organizando os fields, loadings e o controle de paginação por chave identificadora do header.
338
+ */
339
+ columnsResultsModel.value[headerKey] = props.useMarkRaw ? markRaw(newColumnValues) : newColumnValues
340
+
341
+ /*
342
+ * Pode acontecer das options nos fields da segunda página serem diferentes da primeira página,
343
+ * portanto deve ocorrer o merge.
344
+ */
345
+ if (response.data?.fields) {
346
+ columnsFieldsModel.value[headerKey] = markRaw(
347
+ getMergedFields(columnsFieldsModel.value[headerKey], response.data?.fields)
348
+ )
349
+ }
350
+
351
+ columnsPagination.value[headerKey].offset = columnsResultsModel.value[headerKey].length
352
+ columnsPagination.value[headerKey].count = response.data?.count
353
+
354
+ emit('fetch-column-success', { response, header })
355
+ }
356
+
357
+ /*
358
+ * Mergeia os options antigos com os novos de cada field.
359
+ */
360
+ function getMergedFields (oldFields, newFields) {
361
+ // Primeira vez batendo a API, retorna os novos fields.
362
+ if (!oldFields || !props.lazyLoadingFieldsKeys.length) return newFields
363
+
364
+ // Caso bata a API e por algum motivo não venha fields, mantenha o antigos.
365
+ if (oldFields && !newFields) return oldFields
366
+
367
+ const mergedFields = { ...oldFields }
368
+
369
+ props.lazyLoadingFieldsKeys.forEach(fieldKey => {
370
+ mergedFields[fieldKey].options = getNonDuplicatedOptions(oldFields[fieldKey].options, newFields[fieldKey].options)
371
+ })
372
+
373
+ return mergedFields
374
+ }
375
+
376
+ /*
377
+ * Tratamento para fazer o merge e evitar options duplicados.
378
+ */
379
+ function getNonDuplicatedOptions (oldOptions, newOptions) {
380
+ const options = [...oldOptions]
381
+
382
+ newOptions.forEach(item => {
383
+ const hasOption = options.find(option => option.value === item.value)
384
+
385
+ if (!hasOption) options.push(item)
386
+ })
387
+
388
+ return options
389
+ }
390
+
391
+ function getItemsByHeader (header) {
392
+ return hasColumnsLength.value ? columnsResultsModel.value[getKeyByHeader(header)] : []
393
+ }
394
+
395
+ /**
396
+ * Pegar key com base na chave identificador, exemplo:
397
+ * header -> { date: '2024-02-12', ... }
398
+ * columnIdKey -> 'date'
399
+ * retorno -> '2024-02-12'
400
+ *
401
+ * Onde esta chave será o "id" da coluna, sendo usado para bater a API, lidar com paginação, loading, etc.
402
+ *
403
+ * @example getKeyByHeader({ date: '2024-02-12', ... })
404
+ * @returns {string} // '2024-02-12'
405
+ */
406
+ function getKeyByHeader (header = {}) {
407
+ return header[props.columnIdKey]
408
+ }
409
+
410
+ /*
411
+ * Para cada header, irá ser criado um item com sua chave identificadora para lidar com paginação e loading.
412
+ * columnsPagination -> { '2024-02-15': { limit: 12, offset: 0 }, '2024-02-16': { limit: 12, offset: 0 }, ... }
413
+ * columnsLoading ->{ '2024-02-15': false, '2024-02-16': false, ... }
414
+ */
415
+ function setColumnsPagination () {
416
+ columnsPagination.value = {}
417
+ columnsLoading.value = {}
418
+
419
+ props.headers.forEach(header => {
420
+ const headerKey = getKeyByHeader(header)
421
+
422
+ columnsPagination.value[headerKey] = { limit: props.limitPerColumn, offset: 0 }
423
+ columnsLoading.value[headerKey] = false
424
+ })
425
+ }
426
+
427
+ function fetchColumnsValues () {
428
+ reset()
429
+ setColumnHeightContainer()
430
+ setColumnsPagination()
431
+ fetchColumns()
432
+ }
433
+
434
+ /**
435
+ * Descricao:
436
+ * Exibe o texto quando:
437
+ * - Nao esta carregando a coluna
438
+ * - Nao tem itens na coluna
439
+ * - Nao estou fazendo o drag and drop
440
+ *
441
+ * @param {Object} header
442
+ */
443
+ function hasEmptyResultText (header) {
444
+ return !columnsLoading.value[getKeyByHeader(header)] && !getItemsByHeader(header)?.length && !isDragging.value
445
+ }
446
+
447
+ /*
448
+ * Valida se o tamanho dos itens da coluna é menor que o valor total de itens que o back retorna e
449
+ * se a coluna não está em carregamento.
450
+ */
451
+ function hasSeeMore (header) {
452
+ const headerKey = getKeyByHeader(header)
453
+ const hasMorePagination = columnsResultsModel.value[headerKey]?.length < columnsPagination.value[headerKey]?.count
454
+
455
+ return hasMorePagination && !columnsLoading.value[headerKey]
456
+ }
457
+
458
+ function reset () {
459
+ columnsResultsModel.value = {}
460
+ columnsPagination.value = {}
461
+ columnsLoading.value = {}
462
+ }
463
+
464
+ function getFieldsByHeader (header) {
465
+ const headerKey = getKeyByHeader(header)
466
+
467
+ return columnsFieldsModel.value[headerKey] || {}
468
+ }
469
+
470
+ /**
471
+ * Loopa todos os itens da coluna com base no ref para pegar o elemento HTML e setar e instaciar o sortable.
472
+ */
473
+ function handleElementsList () {
474
+ columnContainer.value.forEach((element, index) => {
475
+ const sortable = setSortable(element, index)
476
+
477
+ sortableInstances.value.push(sortable)
478
+ })
479
+ }
480
+
481
+ /**
482
+ * Descricao:
483
+ * Seta a instancia do sortable, no qual varia de acordo com as props passadas.
484
+ *
485
+ * @param {HTMLElement} element
486
+ * @param {Number} index
487
+ */
488
+ function setSortable (element, index) {
489
+ const defaultSortableConfig = {
490
+ animation: 500,
491
+ swapThreshold: 1,
492
+ delay: 50,
493
+ delayOnTouchOnly: true,
494
+ emptyInsertThreshold: 0
495
+ }
496
+
497
+ /**
498
+ * Caso seja apenas drag and drop no eixo Y
499
+ */
500
+ const useOnlyDragAndDropY = !!props.useDragAndDropY && !props.useDragAndDropX
501
+
502
+ const sortable = new Sortable(element, {
503
+ sort: props.useDragAndDropY,
504
+
505
+ ...defaultSortableConfig,
506
+
507
+ ...props.sortableConfig,
508
+
509
+ group: useOnlyDragAndDropY ? `column-${index}` : 'shared',
510
+
511
+ direction: useOnlyDragAndDropY ? 'vertical' : 'horizontal',
512
+
513
+ onStart: toggleIsDragging,
514
+
515
+ onAdd: event => onDropCard(event),
516
+
517
+ ...(props.useDragAndDropY && {
518
+ onSort: event => onDropCard(event)
519
+ })
520
+ })
521
+
522
+ return sortable
523
+ }
524
+
525
+ function toggleIsDragging () {
526
+ isDragging.value = !isDragging.value
527
+ }
528
+
529
+ function onDropCard (event) {
530
+ onCancelDrop.value = () => cancelDrop(event)
531
+
532
+ onConfirmDrop.value = () => confirmDrop(event)
533
+
534
+ hasConfirmDialogProps.value
535
+ ? openConfirmDialog()
536
+ : confirmDrop(event)
537
+ }
538
+
539
+ function openConfirmDialog () {
540
+ showConfirmDialog.value = true
541
+ }
542
+
543
+ function closeConfirmDialog () {
544
+ showConfirmDialog.value = false
545
+ }
546
+
547
+ /**
548
+ * @param {event} event
549
+ */
550
+ function cancelDrop (event) {
551
+ /**
552
+ * Insere na posição antiga que pertencia (event.oldIndex) dentro do seu antigo pai (event.from)
553
+ */
554
+ if (props.useDragAndDropX) event.from.insertBefore(event.item, event.from.children[event.oldIndex])
555
+
556
+ if (props.useDragAndDropY) {
557
+ const oldIndex = event.oldIndex
558
+
559
+ /**
560
+ * Se oldIndex for 0, o targetIndex deverá ser 0, pois isso indica que se o item é o primeiro da lista, ele não será movido para outra posição.
561
+ *
562
+ * Caso o oldIndex for diferente, devo incrementar 1 para adicionar, pois isso permite que o item seja inserido logo após sua posição original.
563
+ */
564
+ const targetIndex = oldIndex === 0 ? oldIndex : oldIndex + 1
565
+
566
+ /**
567
+ * Verifica se o índice alvo é válido, caso contrário, define como o final
568
+ */
569
+ const insertBeforeElement = targetIndex < event.from.children.length
570
+ ? event.from.children[targetIndex]
571
+ : null
572
+
573
+ event.from.insertBefore(event.item, insertBeforeElement)
574
+ }
575
+
576
+ if (hasConfirmDialogProps.value) closeConfirmDialog()
577
+
578
+ toggleIsDragging()
579
+ }
580
+
581
+ function confirmDrop (event) {
582
+ const { from, to, item: { id: itemId } } = event
583
+
584
+ const { headerKey: newHeaderKey } = to.dataset
585
+ const { headerKey: oldHeaderKey } = from.dataset
586
+
587
+ updatePosition({ newHeaderKey, oldHeaderKey, itemId, event })
588
+ }
589
+
590
+ /**
591
+ *
592
+ * @param {{
593
+ * headerKey: string,
594
+ * itemId: string
595
+ * }}
596
+ */
597
+ function removeItemFromList ({ headerKey, itemId }) {
598
+ /**
599
+ * Coluna referente ao model de resultado
600
+ */
601
+ const columnItemList = columnsResultsModel.value[headerKey]
602
+
603
+ /**
604
+ * Busca o item com base em seu ID na lista de itens da coluna
605
+ */
606
+ const itemIndex = columnItemList.findIndex(itemContent => itemContent[props.itemIdKey] === itemId)
607
+
608
+ /**
609
+ * Remove o item da listagem com base no index, sendo que preciso subtrair 1 para pegar o index correto
610
+ */
611
+ columnItemList.splice(itemIndex, 1)
612
+
613
+ /**
614
+ * Remove o item do count da coluna para não mostrar o botão de "Ver mais¨.
615
+ */
616
+ columnsPagination.value[headerKey].count -= 1
617
+ }
618
+
619
+ /**
620
+ * Descricao:
621
+ * Metodo que realiza a request de update
622
+ *
623
+ * @param {{
624
+ * newHeaderKey: string - ID da coluna de destino,
625
+ * oldHeaderKey: string - ID da antiga coluna,
626
+ * itemId: string - ID do meu item a ser movimento,
627
+ * event: event
628
+ * }}
629
+ */
630
+ async function updatePosition ({ newHeaderKey, oldHeaderKey, itemId, event }) {
631
+ const params = {
632
+ [props.columnIdKey]: newHeaderKey,
633
+ ...(props.useDragAndDropY && { newIndex: event.newIndex }),
634
+ ...props.updatePositionParams
635
+ }
636
+
637
+ const { data, error } = await promiseHandler(
638
+ axios.patch(`${props.updatePositionUrl}/${itemId}/update-position`, params),
639
+ {
640
+ errorMessage: 'Ocorreu um erro ao atualizar a posição de seu item.',
641
+ useLoading: false,
642
+ onLoading: value => {
643
+ isLoadingUpdatePosition.value = value
644
+
645
+ columnsLoading.value[newHeaderKey] = value
646
+ }
647
+ }
648
+ )
649
+
650
+ if (error) {
651
+ onCancelDrop.value()
652
+
653
+ return
654
+ }
655
+
656
+ removeItemFromList({ headerKey: oldHeaderKey, itemId })
657
+
658
+ setItemList({ headerKey: newHeaderKey, data: data.data, index: event.newIndex })
659
+
660
+ isUpdatingPosition.value = true
661
+
662
+ toggleIsDragging()
663
+
664
+ closeConfirmDialog()
665
+
666
+ emit('update-success')
667
+ }
668
+
669
+ function setItemList ({ headerKey, data, index }) {
670
+ /**
671
+ * Coluna referente ao model de resultado
672
+ */
673
+ const columnItemList = columnsResultsModel.value[headerKey]
674
+
675
+ /**
676
+ * Adiciona o item na posição do event escolhido.
677
+ */
678
+ columnItemList.splice(index, 0, data.result)
679
+ }
680
+
681
+ function destroySortable () {
682
+ sortableInstances.value.forEach(sortable => sortable.destroy())
683
+ }
684
+ </script>
685
+
686
+ <style lang="scss">
687
+ .qas-board-generator {
688
+ max-height: 100vh;
689
+
690
+ &__column {
691
+ overflow-x: hidden;
692
+ scrollbar-width: none;
693
+
694
+ &:hover {
695
+ scrollbar-width: thin;
696
+
697
+ &::-webkit-scrollbar {
698
+ display: block;
699
+ }
700
+ }
701
+
702
+ &::-webkit-scrollbar {
703
+ display: none;
704
+ }
705
+ }
706
+
707
+ // 60px é o valor do padding definido no container da column.
708
+ &__column-items {
709
+ height: calc(100% - 60px);
710
+ }
711
+ }
712
+ </style>