@bildvitta/quasar-ui-asteroid 3.20.0-beta.2 → 3.20.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.
- package/package.json +3 -3
- package/src/asteroid.js +8 -1
- package/src/components/actions/QasActions.vue +12 -2
- package/src/components/actions-menu/QasActionsMenu.vue +18 -5
- package/src/components/alert/QasAlert.vue +89 -64
- package/src/components/app-user/QasAppUser.vue +2 -1
- package/src/components/board-generator/QasBoardGenerator.vue +883 -162
- package/src/components/board-generator/QasBoardGenerator.yml +83 -2
- package/src/components/board-generator/private/PvBoardGeneratorCardsContainer.vue +25 -0
- package/src/components/box/QasBox.vue +16 -3
- package/src/components/box/QasBox.yml +10 -0
- package/src/components/btn/QasBtn.vue +27 -5
- package/src/components/btn/QasBtn.yml +10 -1
- package/src/components/btn-dropdown/QasBtnDropdown.vue +13 -1
- package/src/components/card/QasCard.vue +97 -25
- package/src/components/card/QasCard.yml +10 -0
- package/src/components/card-image/QasCardImage.vue +10 -1
- package/src/components/card-image/QasCardImage.yml +5 -0
- package/src/components/chart-view/QasChartView.vue +4 -3
- package/src/components/chart-view/QasChartView.yml +5 -0
- package/src/components/checkbox/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/components/copy/QasCopy.vue +6 -1
- package/src/components/copy/QasCopy.yml +5 -0
- package/src/components/date-time-input/QasDateTimeInput.vue +30 -6
- package/src/components/dialog/QasDialog.vue +308 -91
- package/src/components/dialog/QasDialog.yml +51 -23
- package/src/components/dialog/composables/use-cancel.js +1 -1
- package/src/components/dialog/composables/use-dynamic-components.js +2 -2
- package/src/components/dialog/composables/use-ok.js +1 -0
- package/src/components/dialog-router/QasDialogRouter.vue +1 -1
- package/src/components/drawer/QasDrawer.vue +76 -26
- package/src/components/drawer/QasDrawer.yml +10 -0
- package/src/components/expansion-item/QasExpansionItem.yml +5 -0
- package/src/components/filters/QasFilters.vue +2 -1
- package/src/components/filters/private/PvFiltersActions.vue +79 -13
- package/src/components/form-generator/QasFormGenerator.vue +8 -1
- package/src/components/form-generator/QasFormGenerator.yml +10 -0
- package/src/components/form-view/QasFormView.vue +20 -11
- package/src/components/form-view/QasFormView.yml +6 -0
- package/src/components/gallery/composables/use-delete.js +2 -3
- package/src/components/gallery/private/PvGalleryCarouselDialog.vue +8 -7
- package/src/components/grid-item/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/components/header/QasHeader.vue +66 -11
- package/src/components/header/QasHeader.yml +16 -1
- package/src/components/infinite-scroll/QasInfiniteScroll.vue +1 -1
- package/src/components/label/QasLabel.vue +3 -1
- package/src/components/layout/QasLayout.vue +16 -1
- package/src/components/layout/private/PvLayoutNotificationsDrawer.vue +2 -1
- package/src/components/layout/private/PvLayoutOverlayDrawer.vue +4 -2
- package/src/components/lazy-loading-components/QasLazyLoadingComponents.vue +262 -0
- package/src/components/lazy-loading-components/QasLazyLoadingComponents.yml +49 -0
- package/src/components/list-view/QasListView.vue +12 -4
- package/src/components/list-view/QasListView.yml +12 -0
- package/src/components/page-header/QasPageHeader.vue +49 -3
- package/src/components/page-header/QasPageHeader.yml +5 -0
- package/src/components/router-link/QasRouterLink.vue +72 -0
- package/src/components/router-link/QasRouterLink.yml +24 -0
- package/src/components/search-box/QasSearchBox.vue +1 -1
- package/src/components/select/QasSelect.vue +8 -1
- package/src/components/select-list-dialog/QasSelectListDialog.vue +40 -20
- package/src/components/select-list-dialog/QasSelectListDialog.yml +14 -2
- package/src/components/signature-uploader/QasSignatureUploader.vue +5 -18
- package/src/components/single-view/QasSingleView.vue +2 -2
- package/src/components/skeleton/QasSkeleton.vue +139 -0
- package/src/components/skeleton/QasSkeleton.yml +48 -0
- package/src/components/sortable/QasSortable.vue +1 -1
- package/src/components/stepper/QasStepper.vue +24 -2
- package/src/components/table-generator/QasTableGenerator.vue +186 -35
- package/src/components/table-generator/QasTableGenerator.yml +6 -1
- package/src/components/tabs-generator/QasTabsGenerator.vue +14 -3
- package/src/components/tabs-generator/QasTabsGenerator.yml +5 -1
- package/src/components/text-truncate/QasTextTruncate.vue +61 -12
- package/src/components/text-truncate/QasTextTruncate.yml +5 -0
- package/src/components/toggle-visibility/QasToggleVisibility.vue +2 -1
- package/src/components/tooltip/QasTooltip.vue +6 -1
- package/src/components/tree-generator/QasTreeGenerator.vue +4 -6
- package/src/components/uploader/QasUploader.vue +12 -2
- package/src/composables/private/use-view.js +1 -1
- package/src/composables/use-overlay-navigation.js +116 -10
- package/src/composables/use-screen.js +17 -1
- package/src/css/components/button.scss +82 -3
- package/src/css/components/item.scss +6 -0
- package/src/css/utils/background.scss +5 -0
- package/src/css/utils/border.scss +6 -0
- package/src/css/utils/container.scss +4 -3
- package/src/css/utils/text.scss +9 -0
- package/src/helpers/copy-to-clipboard.js +2 -1
- package/src/helpers/filters.js +1 -1
- package/src/helpers/promise-handler.js +2 -1
- package/src/helpers/set-scroll-gradient.js +31 -8
- package/src/helpers/set-scroll-on-grab.js +10 -3
- package/src/index.scss +1 -0
- package/src/mixins/search-filter.js +7 -1
- package/src/plugins/delete/Delete.js +7 -9
- package/src/plugins/delete/Delete.yml +1 -1
- package/src/plugins/dialog/Dialog.yml +1 -1
- package/src/plugins/notify-error/NotifyError.yml +1 -1
- package/src/plugins/notify-success/NotifySuccess.yml +1 -1
- package/src/plugins/screen/Screen.js +17 -1
- package/src/plugins/screen/Screen.yml +5 -1
- package/src/vue-plugin.js +5 -7
- package/src/plugins/index.js +0 -5
|
@@ -1,40 +1,96 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<div
|
|
5
|
-
<qas-
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
<div>
|
|
3
|
+
<qas-grabbable class="qas-board-generator" v-bind="grabbableProps">
|
|
4
|
+
<div ref="columnsContainer" class="no-wrap q-gutter-md q-pb-xs q-px-lg row">
|
|
5
|
+
<qas-lazy-loading-components :key="lazyLoadingKey" v-model:visible-items="visibleItems" direction="horizontal" placeholder-width="350px" :threshold="0">
|
|
6
|
+
<qas-box v-for="(header, index) in normalizedHeaders" :key="index" :class="getColumnClasses(header)" :style="containerStyle">
|
|
7
|
+
<div class="ellipsis q-mb-md text-grey-10">
|
|
8
|
+
<qas-skeleton v-if="props.skeleton" type="text" use-contrast width="80%" />
|
|
9
|
+
|
|
10
|
+
<slot v-else :fields="getFieldsByHeader(header)" :header="header" :index="index" name="header-column" />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<pv-board-generator-cards-container ref="columnContainer" class="qas-board-generator__column secondary-scroll" v-bind="getCardsContainerProps(header)">
|
|
14
|
+
<!-- COLUNA COM ERRO -->
|
|
15
|
+
<div v-if="columnsWithError[getKeyByHeader(header)]" class="column full-height items-center justify-center">
|
|
16
|
+
<div class="text-center">
|
|
17
|
+
<q-icon color="negative" name="sym_r_error" size="md" />
|
|
18
|
+
|
|
19
|
+
<div class="q-mt-sm text-subtitle1">
|
|
20
|
+
{{ props.errorColumnText }}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="text-center">
|
|
25
|
+
<qas-btn class="q-mt-md" icon="sym_r_refresh" label="Tentar novamente" :loading="columnsLoading[getKeyByHeader(header)]" @click="handleFetchColumnClick(header)" />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<template v-else>
|
|
30
|
+
<qas-lazy-loading-components :threshold="0">
|
|
31
|
+
<div v-for="(item) in getItemsByHeader(header)" :id="item[props.itemIdKey]" :key="item[props.itemIdKey]" class="qas-board-generator__item" :data-disable-drag="updatingPositionItemKeys.has(item[props.itemIdKey])">
|
|
32
|
+
<slot v-if="!props.skeleton" :column-index="index" :fields="getFieldsByHeader(header)" :header="header" :is-updating-position="updatingPositionItemKeys.has(item[props.itemIdKey])" :item="item" name="column-item" />
|
|
33
|
+
</div>
|
|
34
|
+
</qas-lazy-loading-components>
|
|
35
|
+
|
|
36
|
+
<div class="full-width justify-center row">
|
|
37
|
+
<qas-btn v-if="hasSeeMore(header)" icon="sym_r_add" :label="props.seeMoreButtonLabel" :loading="columnsLoading[getKeyByHeader(header)]" :use-label-on-small-screen="false" variant="tertiary" @click="handleFetchColumnClick(header)" />
|
|
38
|
+
|
|
39
|
+
<template v-if="hasSkeletonByHeader(header)">
|
|
40
|
+
<div class="q-col-gutter-y-sm row">
|
|
41
|
+
<div v-for="item in skeletonCards" :key="item[props.itemIdKey]" class="col-12">
|
|
42
|
+
<qas-card v-bind="item">
|
|
43
|
+
<template #default />
|
|
44
|
+
</qas-card>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<qas-empty-result-text v-if="hasEmptyResultText(header)" />
|
|
51
|
+
</template>
|
|
52
|
+
</pv-board-generator-cards-container>
|
|
53
|
+
</qas-box>
|
|
54
|
+
</qas-lazy-loading-components>
|
|
22
55
|
</div>
|
|
23
|
-
</div>
|
|
24
56
|
|
|
25
|
-
|
|
26
|
-
|
|
57
|
+
<qas-dialog v-model="showConfirmDialog" v-bind="defaultConfirmDialogProps" />
|
|
58
|
+
</qas-grabbable>
|
|
59
|
+
|
|
60
|
+
<q-inner-loading :showing="loading">
|
|
61
|
+
<q-spinner
|
|
62
|
+
color="grey"
|
|
63
|
+
size="3em"
|
|
64
|
+
/>
|
|
65
|
+
</q-inner-loading>
|
|
66
|
+
</div>
|
|
27
67
|
</template>
|
|
28
68
|
|
|
29
69
|
<script setup>
|
|
70
|
+
import PvBoardGeneratorCardsContainer from './private/PvBoardGeneratorCardsContainer.vue'
|
|
71
|
+
import QasSkeleton from '../skeleton/QasSkeleton.vue'
|
|
72
|
+
import QasCard from '../card/QasCard.vue'
|
|
30
73
|
import QasBox from '../box/QasBox.vue'
|
|
31
74
|
import QasBtn from '../btn/QasBtn.vue'
|
|
32
75
|
import QasDialog from '../dialog/QasDialog.vue'
|
|
33
76
|
import QasEmptyResultText from '../empty-result-text/QasEmptyResultText.vue'
|
|
34
77
|
import QasGrabbable from '../grabbable/QasGrabbable.vue'
|
|
78
|
+
import QasLazyLoadingComponents from '../lazy-loading-components/QasLazyLoadingComponents.vue'
|
|
35
79
|
|
|
36
|
-
import { ref, watch, computed, onUnmounted, markRaw, inject, onMounted } from 'vue'
|
|
37
80
|
import promiseHandler from '../../helpers/promise-handler'
|
|
81
|
+
import NotifyError from '../../plugins/notify-error/NotifyError'
|
|
82
|
+
|
|
83
|
+
import {
|
|
84
|
+
ref,
|
|
85
|
+
watch,
|
|
86
|
+
computed,
|
|
87
|
+
onBeforeUnmount,
|
|
88
|
+
markRaw,
|
|
89
|
+
inject,
|
|
90
|
+
provide,
|
|
91
|
+
onMounted,
|
|
92
|
+
nextTick
|
|
93
|
+
} from 'vue'
|
|
38
94
|
|
|
39
95
|
import Sortable from 'sortablejs'
|
|
40
96
|
|
|
@@ -56,11 +112,6 @@ const props = defineProps({
|
|
|
56
112
|
default: () => ({})
|
|
57
113
|
},
|
|
58
114
|
|
|
59
|
-
headerBoxProps: {
|
|
60
|
-
type: Object,
|
|
61
|
-
default: () => ({})
|
|
62
|
-
},
|
|
63
|
-
|
|
64
115
|
columnIdKey: {
|
|
65
116
|
type: String,
|
|
66
117
|
required: true
|
|
@@ -81,6 +132,11 @@ const props = defineProps({
|
|
|
81
132
|
default: () => ({})
|
|
82
133
|
},
|
|
83
134
|
|
|
135
|
+
errorColumnText: {
|
|
136
|
+
type: String,
|
|
137
|
+
default: 'Não foi possível carregar os itens desta coluna.'
|
|
138
|
+
},
|
|
139
|
+
|
|
84
140
|
height: {
|
|
85
141
|
type: String,
|
|
86
142
|
default: ''
|
|
@@ -96,9 +152,18 @@ const props = defineProps({
|
|
|
96
152
|
default: 12
|
|
97
153
|
},
|
|
98
154
|
|
|
155
|
+
loading: {
|
|
156
|
+
type: Boolean
|
|
157
|
+
},
|
|
158
|
+
|
|
99
159
|
columnWidth: {
|
|
100
160
|
type: String,
|
|
101
|
-
default: '
|
|
161
|
+
default: '350px'
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
seeMoreButtonLabel: {
|
|
165
|
+
type: String,
|
|
166
|
+
default: 'Ver mais'
|
|
102
167
|
},
|
|
103
168
|
|
|
104
169
|
sortableConfig: {
|
|
@@ -106,9 +171,8 @@ const props = defineProps({
|
|
|
106
171
|
default: () => ({})
|
|
107
172
|
},
|
|
108
173
|
|
|
109
|
-
|
|
110
|
-
type: Boolean
|
|
111
|
-
default: true
|
|
174
|
+
skeleton: {
|
|
175
|
+
type: Boolean
|
|
112
176
|
},
|
|
113
177
|
|
|
114
178
|
useDragAndDropX: {
|
|
@@ -120,7 +184,7 @@ const props = defineProps({
|
|
|
120
184
|
},
|
|
121
185
|
|
|
122
186
|
updatePositionUrl: {
|
|
123
|
-
type: String,
|
|
187
|
+
type: [String, Function],
|
|
124
188
|
default: ''
|
|
125
189
|
},
|
|
126
190
|
|
|
@@ -145,23 +209,49 @@ const emit = defineEmits([
|
|
|
145
209
|
'update-error'
|
|
146
210
|
])
|
|
147
211
|
|
|
148
|
-
defineExpose({
|
|
212
|
+
defineExpose({
|
|
213
|
+
fetchColumns,
|
|
214
|
+
fetchColumn,
|
|
215
|
+
reset,
|
|
216
|
+
cancelDrop,
|
|
217
|
+
refreshColumn,
|
|
218
|
+
transferItemToColumn,
|
|
219
|
+
removeItemFromList,
|
|
220
|
+
updateItemInList,
|
|
221
|
+
refetchColumns: fetchColumnsValues
|
|
222
|
+
})
|
|
149
223
|
|
|
150
|
-
//
|
|
224
|
+
// globals
|
|
151
225
|
const axios = inject('axios')
|
|
152
226
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const isInsideListView = inject('isListView', false)
|
|
156
|
-
|
|
157
|
-
// Refs
|
|
227
|
+
// refs
|
|
158
228
|
const columnContainer = ref(null)
|
|
159
229
|
const columnsPagination = ref({})
|
|
160
230
|
const columnsLoading = ref({})
|
|
161
231
|
const columnsFieldsModel = ref({})
|
|
162
232
|
const showConfirmDialog = ref(false)
|
|
163
|
-
const isDragging = ref(false)
|
|
164
233
|
const isLoadingUpdatePosition = ref(false)
|
|
234
|
+
const hideSkeleton = ref(false)
|
|
235
|
+
const columnsWithError = ref({})
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Set de IDs dos itens com update de posição em andamento.
|
|
239
|
+
* Usamos Set para evitar duplicatas e ter O(1) no has().
|
|
240
|
+
*/
|
|
241
|
+
const updatingPositionItemKeys = ref(new Set())
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Índices das colunas visíveis no viewport
|
|
245
|
+
* Populado pelo QasLazyLoadingComponents via v-model:visible-items
|
|
246
|
+
*/
|
|
247
|
+
const visibleItems = ref([])
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Chave reativa para forçar o remount do QasLazyLoadingComponents externo.
|
|
251
|
+
* Ao incrementar, o componente é destruído e recriado com estado limpo,
|
|
252
|
+
* evitando render de VNodes obsoletos que causam crash.
|
|
253
|
+
*/
|
|
254
|
+
const lazyLoadingKey = ref(0)
|
|
165
255
|
|
|
166
256
|
/**
|
|
167
257
|
* Instâncias do sortable, que são utilizadas para realizar o destroy ao sair da página
|
|
@@ -179,7 +269,67 @@ const onConfirmDrop = ref(() => {})
|
|
|
179
269
|
*/
|
|
180
270
|
const isUpdatingPosition = ref(false)
|
|
181
271
|
|
|
182
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Contador de drags ativos. Usado em vez de um booleano com toggle para
|
|
274
|
+
* evitar race conditions quando múltiplos drags acontecem em sequência rápida.
|
|
275
|
+
* isDragging continua existindo como computed derivado deste contador.
|
|
276
|
+
*/
|
|
277
|
+
const draggingCount = ref(0)
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Fila de chamadas de updatePosition para garantir execução sequencial.
|
|
281
|
+
* Cada entrada é uma função que retorna uma Promise (a chamada updatePosition).
|
|
282
|
+
* Isso evita race conditions causadas por múltiplas requests concorrentes
|
|
283
|
+
* que compartilham estado (updatingPositionItemKey, isLoadingUpdatePosition, etc).
|
|
284
|
+
*/
|
|
285
|
+
let updatePositionQueue = Promise.resolve()
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Mapa de AbortControllers por chave de coluna para cancelar requests em andamento.
|
|
289
|
+
* Não é reativo pois não precisamos de reatividade sobre os controllers.
|
|
290
|
+
*/
|
|
291
|
+
let columnAbortControllers = {}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Contador de sessão para o fetchColumns em execução.
|
|
295
|
+
* Incrementado a cada nova chamada, permitindo que sessões antigas detectem que foram substituídas.
|
|
296
|
+
*/
|
|
297
|
+
let currentFetchColumnsSession = 0
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Estado do smooth scroll horizontal.
|
|
301
|
+
* Acumula o target e interpola suavemente via requestAnimationFrame (rAF).
|
|
302
|
+
*
|
|
303
|
+
* rAF (requestAnimationFrame) sincroniza a animação com o refresh da tela (~60fps).
|
|
304
|
+
* Isso evita "saltos" de scroll, tornando o movimento suave e fluido.
|
|
305
|
+
*/
|
|
306
|
+
const SMOOTH_SCROLL_LERP = 0.2
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Velocidade de scroll vertical (dentro da coluna), independente do scrollSpeed horizontal.
|
|
310
|
+
* O offsetY recebido no scrollFn é proporcional ao scrollSpeed (60), então normalizamos
|
|
311
|
+
* para que o scroll vertical se comporte como se scrollSpeed fosse 10 (padrão do SortableJS).
|
|
312
|
+
*/
|
|
313
|
+
const HORIZONTAL_SCROLL_SPEED = 60
|
|
314
|
+
const VERTICAL_SCROLL_SPEED = 20
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Sensibilidade para iniciar o scroll automático ao arrastar próximo às bordas do container.
|
|
318
|
+
*/
|
|
319
|
+
const HORIZONTAL_SCROLL_SENSITIVITY = 150
|
|
320
|
+
const VERTICAL_SCROLL_SENSITIVITY = 80
|
|
321
|
+
|
|
322
|
+
let smoothScrollTarget = 0
|
|
323
|
+
let smoothScrollRafId = null
|
|
324
|
+
let smoothScrollEl = null
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Geração por coluna para invalidar callbacks onLoading de requests canceladas.
|
|
328
|
+
* Evita que o onLoading(false) de uma request cancelada sobrescreva o estado da nova request.
|
|
329
|
+
*/
|
|
330
|
+
const columnFetchGenerations = {}
|
|
331
|
+
|
|
332
|
+
// consts
|
|
183
333
|
const hasDragAndDrop = !!props.useDragAndDropX || !!props.useDragAndDropY
|
|
184
334
|
|
|
185
335
|
const grabbableProps = {
|
|
@@ -190,44 +340,38 @@ const grabbableProps = {
|
|
|
190
340
|
})
|
|
191
341
|
}
|
|
192
342
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Gera cards de skeleton para exibir enquanto carrega os itens da coluna, o mesmo é usado para preencher a coluna
|
|
345
|
+
* quando a prop "skeleton" for true, indicando que é para simular o carregamento.
|
|
346
|
+
*/
|
|
347
|
+
const skeletonCards = Array.from({ length: 6 }).map(() => {
|
|
348
|
+
return {
|
|
349
|
+
skeleton: true,
|
|
350
|
+
title: '-',
|
|
351
|
+
expansionProps: { label: '-' },
|
|
352
|
+
actionsMenuProps: { list: {} },
|
|
353
|
+
useSelection: true
|
|
204
354
|
}
|
|
205
|
-
)
|
|
355
|
+
})
|
|
206
356
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
(
|
|
210
|
-
|
|
357
|
+
// computeds
|
|
358
|
+
const columnContainerElements = computed(() => {
|
|
359
|
+
return columnContainer.value?.map(columnProxy => columnProxy.$el) || []
|
|
360
|
+
})
|
|
211
361
|
|
|
212
|
-
|
|
362
|
+
const normalizedHeaders = computed(() => {
|
|
363
|
+
// retorna dados fakes para criar colunas de skeleton, caso a prop skeleton seja true.
|
|
364
|
+
if (props.skeleton) {
|
|
365
|
+
return Array.from({ length: 8 }).map((_, index) => {
|
|
366
|
+
return {
|
|
367
|
+
[props.columnIdKey]: `${props.columnIdKey}-${index}`
|
|
368
|
+
}
|
|
369
|
+
})
|
|
213
370
|
}
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
watch(columnContainer, setColumnHeightContainer)
|
|
217
|
-
|
|
218
|
-
// Lifecycles
|
|
219
|
-
onMounted(() => {
|
|
220
|
-
/**
|
|
221
|
-
* Caso eu use o listView (valor pego por provide), a request é feito pelo watch quando se ocorre o sucesso do `fetchList`
|
|
222
|
-
*/
|
|
223
|
-
if (isInsideListView) return
|
|
224
371
|
|
|
225
|
-
|
|
372
|
+
return props.headers
|
|
226
373
|
})
|
|
227
374
|
|
|
228
|
-
onUnmounted(destroySortable)
|
|
229
|
-
|
|
230
|
-
// Computeds
|
|
231
375
|
const columnsResultsModel = computed({
|
|
232
376
|
get () {
|
|
233
377
|
return props.results
|
|
@@ -238,12 +382,12 @@ const columnsResultsModel = computed({
|
|
|
238
382
|
}
|
|
239
383
|
})
|
|
240
384
|
|
|
385
|
+
const isDragging = computed(() => draggingCount.value > 0)
|
|
386
|
+
|
|
241
387
|
const hasColumnsLength = computed(() => !!Object.keys(columnsResultsModel.value).length)
|
|
242
388
|
|
|
243
389
|
const containerStyle = computed(() => `width: ${props.columnWidth};`)
|
|
244
390
|
|
|
245
|
-
const hasConfirmDialogProps = computed(() => !!Object.keys(props.confirmDialogProps).length)
|
|
246
|
-
|
|
247
391
|
const defaultConfirmDialogProps = computed(() => {
|
|
248
392
|
const defaultProps = {
|
|
249
393
|
ok: {
|
|
@@ -263,49 +407,211 @@ const defaultConfirmDialogProps = computed(() => {
|
|
|
263
407
|
}
|
|
264
408
|
})
|
|
265
409
|
|
|
410
|
+
// provide
|
|
411
|
+
provide('isDragging', isDragging)
|
|
412
|
+
|
|
413
|
+
// watchers
|
|
414
|
+
watch(
|
|
415
|
+
() => normalizedHeaders.value,
|
|
416
|
+
value => {
|
|
417
|
+
if (isLoadingUpdatePosition.value || !value?.length) return
|
|
418
|
+
|
|
419
|
+
fetchColumnsValues()
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
watch(() => columnContainerElements.value, () => {
|
|
424
|
+
setColumnHeightContainer()
|
|
425
|
+
handleElementsList()
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// hooks
|
|
429
|
+
onMounted(() => {
|
|
430
|
+
if (normalizedHeaders.value.length) {
|
|
431
|
+
fetchColumnsValues()
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
window.addEventListener('resize', setColumnHeightContainer)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
onBeforeUnmount(() => {
|
|
438
|
+
abortAllColumnRequests()
|
|
439
|
+
destroySortable()
|
|
440
|
+
|
|
441
|
+
window.removeEventListener('resize', setColumnHeightContainer)
|
|
442
|
+
})
|
|
443
|
+
|
|
266
444
|
// functions
|
|
267
445
|
/*
|
|
268
446
|
* Setar o tamanho do container do board, onde deverá ser a altura passada via prop, ou o default será ocupar o maximo
|
|
269
447
|
* de espaço que ele conseguir considerando a altura do container em relação ao topo.
|
|
270
448
|
*/
|
|
271
449
|
function setColumnHeightContainer () {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
450
|
+
if (props.height) {
|
|
451
|
+
columnContainerElements.value.forEach(columnElement => {
|
|
452
|
+
columnElement.style.height = props.height
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Primeira etapa: calcula e aplica a altura inicial de cada coluna
|
|
459
|
+
columnContainerElements.value.forEach(columnElement => {
|
|
460
|
+
// Pega a posição atual da coluna em relação ao topo da viewport
|
|
461
|
+
const rect = columnElement.getBoundingClientRect()
|
|
462
|
+
const heightToTop = rect.top
|
|
463
|
+
|
|
464
|
+
// clientHeight dá a altura da viewport SEM incluir as scrollbars
|
|
465
|
+
const viewportHeight = document.documentElement.clientHeight
|
|
276
466
|
|
|
277
|
-
|
|
467
|
+
// Padding inferior aplicado no container para dar espaçamento visual
|
|
468
|
+
const paddingBottom = 8
|
|
469
|
+
|
|
470
|
+
// Calcula quanto de espaço temos disponível do topo da coluna até o fim da tela
|
|
471
|
+
const availableHeight = viewportHeight - heightToTop - paddingBottom
|
|
472
|
+
|
|
473
|
+
// Aplica essa altura na coluna
|
|
474
|
+
columnElement.style.height = `${availableHeight}px`
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// Segunda etapa: após o DOM atualizar, verifica se algum scroll vertical foi criado
|
|
478
|
+
nextTick(() => {
|
|
479
|
+
/**
|
|
480
|
+
* scrollHeight é a altura total do conteúdo (incluindo o que não está visível)
|
|
481
|
+
* clientHeight é a altura visível da viewport
|
|
482
|
+
* Se scrollHeight > clientHeight, significa que há conteúdo "sobrando" e foi criado scroll vertical.
|
|
483
|
+
*/
|
|
484
|
+
const hasVerticalScroll = document.documentElement.scrollHeight > document.documentElement.clientHeight
|
|
485
|
+
|
|
486
|
+
if (hasVerticalScroll) {
|
|
487
|
+
// Calcula exatamente quantos pixels estão "sobrando" e causando o scroll
|
|
488
|
+
const adjustment = document.documentElement.scrollHeight - document.documentElement.clientHeight
|
|
489
|
+
|
|
490
|
+
// Reduz a altura de todas as colunas pelo valor exato do scroll + 2px de margem de segurança
|
|
491
|
+
columnContainerElements.value.forEach(columnElement => {
|
|
492
|
+
const safetyMargin = 2
|
|
493
|
+
|
|
494
|
+
const currentHeight = parseInt(columnElement.style.height, 10)
|
|
495
|
+
columnElement.style.height = `${currentHeight - adjustment - safetyMargin}px`
|
|
496
|
+
})
|
|
497
|
+
}
|
|
278
498
|
})
|
|
279
499
|
}
|
|
280
500
|
|
|
281
501
|
/*
|
|
282
502
|
* Bater API pra cada header
|
|
503
|
+
* Etapa 1: faz as requests das colunas visíveis no carregamento inicial
|
|
504
|
+
* Etapa 2: após finalizar, faz as requests das colunas não visíveis
|
|
283
505
|
*/
|
|
284
506
|
async function fetchColumns () {
|
|
285
|
-
|
|
507
|
+
if (props.skeleton) return
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Cada chamada a fetchColumns recebe um ID de sessão único.
|
|
511
|
+
* Se um novo fetchColumnsValues for chamado durante a execução, a sessão anterior
|
|
512
|
+
* detecta que foi substituída e para de processar, evitando emissão de eventos falsos.
|
|
513
|
+
*/
|
|
514
|
+
const mySession = ++currentFetchColumnsSession
|
|
286
515
|
|
|
287
|
-
|
|
516
|
+
// Mapeia os índices visíveis para os IDs de coluna correspondentes
|
|
517
|
+
const visibleKeys = new Set(visibleItems.value.map(item => getKeyByHeader(normalizedHeaders.value[item])))
|
|
288
518
|
|
|
289
|
-
|
|
290
|
-
|
|
519
|
+
const visibleHeaders = visibleKeys.size
|
|
520
|
+
? normalizedHeaders.value.filter(header => visibleKeys.has(getKeyByHeader(header)))
|
|
521
|
+
: normalizedHeaders.value
|
|
522
|
+
|
|
523
|
+
const hiddenHeaders = visibleKeys.size
|
|
524
|
+
? normalizedHeaders.value.filter(header => !visibleKeys.has(getKeyByHeader(header)))
|
|
525
|
+
: []
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Isto é necessário para setar o loading das colunas ocultas mesmo antes de elas serem visíveis
|
|
529
|
+
* e suas requests serem feitas, garantindo que ao entrar no viewport elas já estejam com o estado
|
|
530
|
+
* de loading correto, evitando bugs visuais.
|
|
531
|
+
*/
|
|
532
|
+
hiddenHeaders.forEach(header => {
|
|
533
|
+
const headerKey = getKeyByHeader(header)
|
|
534
|
+
|
|
535
|
+
columnsLoading.value[headerKey] = true
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
// Etapa 1: colunas visíveis (prioridade)
|
|
539
|
+
const visibleColumns = await Promise.allSettled(visibleHeaders.map(header => fetchColumn(header, false)))
|
|
540
|
+
|
|
541
|
+
// Sessão foi substituída por um novo fetchColumnsValues — encerra sem emitir eventos
|
|
542
|
+
if (currentFetchColumnsSession !== mySession) return
|
|
543
|
+
|
|
544
|
+
// Etapa 2: colunas não visíveis (só executa após etapa 1 finalizar, menor prioridade)
|
|
545
|
+
const hiddenColumns = await Promise.allSettled(hiddenHeaders.map(header => fetchColumn(header, false)))
|
|
546
|
+
|
|
547
|
+
// Verifica novamente pois outro fetchColumnsValues pode ter sido chamado durante a etapa 2
|
|
548
|
+
if (currentFetchColumnsSession !== mySession) return
|
|
549
|
+
|
|
550
|
+
const allPromises = [...visibleColumns, ...hiddenColumns]
|
|
551
|
+
console.log('🚀 ~ fetchColumns ~ allPromises:', allPromises)
|
|
552
|
+
|
|
553
|
+
const hasAllPromisesSucceeded = allPromises.every(promise => promise.status === 'fulfilled')
|
|
554
|
+
const hasAllPromisesFailed = allPromises.length > 0 && allPromises.every(promise => promise.status === 'rejected')
|
|
555
|
+
|
|
556
|
+
if (hasAllPromisesFailed) {
|
|
557
|
+
emit('fetch-columns-error')
|
|
558
|
+
|
|
559
|
+
NotifyError('Ocorreu um erro ao carregar as colunas. Tente novamente mais tarde.')
|
|
291
560
|
|
|
292
561
|
return
|
|
293
562
|
}
|
|
294
563
|
|
|
295
|
-
|
|
564
|
+
if (hasAllPromisesSucceeded) {
|
|
565
|
+
emit('fetch-columns-success')
|
|
566
|
+
}
|
|
296
567
|
|
|
297
568
|
if (hasDragAndDrop) handleElementsList()
|
|
298
569
|
}
|
|
299
570
|
|
|
300
571
|
/*
|
|
301
|
-
*
|
|
572
|
+
* Wrapper para chamadas de fetchColumn originadas de eventos de clique do usuário.
|
|
573
|
+
* fetchColumn relança o erro para que Promise.allSettled em fetchColumns consiga
|
|
574
|
+
* detectar colunas com falha; aqui o erro já foi tratado internamente, portanto
|
|
575
|
+
* é suprimido para evitar o warning "Unhandled error during execution of component
|
|
576
|
+
* event handler" do Vue.
|
|
302
577
|
*/
|
|
303
|
-
|
|
578
|
+
function handleFetchColumnClick (header) {
|
|
579
|
+
fetchColumn(header, true)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Busca a coluna com base no header recebido.
|
|
584
|
+
*
|
|
585
|
+
* @param header - payload do header da coluna a ser buscada, exemplo: { date: '2024-02-12', ... }
|
|
586
|
+
* @param shouldHideSkeleton - flag para controlar se o skeleton de carregamento deve ser ocultado durante a busca
|
|
587
|
+
*/
|
|
588
|
+
async function fetchColumn (header, shouldHideSkeleton) {
|
|
304
589
|
const headerKey = getKeyByHeader(header)
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Cancela qualquer request anterior para esta mesma coluna antes de iniciar a nova.
|
|
593
|
+
* Garante que nunca haja duas requests paralelas para a mesma coluna.
|
|
594
|
+
*/
|
|
595
|
+
abortColumnRequest(headerKey)
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Incrementa a geração desta coluna para invalidar callbacks onLoading da request cancelada,
|
|
599
|
+
* evitando que o onLoading(false) antigo sobrescreva o estado da nova request.
|
|
600
|
+
* Usa ?? 0 pois ++undefined retorna NaN, e NaN === NaN é sempre false, travando o skeleton.
|
|
601
|
+
*/
|
|
602
|
+
columnFetchGenerations[headerKey] = (columnFetchGenerations[headerKey] ?? 0) + 1
|
|
603
|
+
const generation = columnFetchGenerations[headerKey]
|
|
604
|
+
|
|
605
|
+
const abortController = new AbortController()
|
|
606
|
+
columnAbortControllers[headerKey] = abortController
|
|
607
|
+
|
|
305
608
|
const { limit, offset } = columnsPagination.value[headerKey] || {}
|
|
306
609
|
|
|
610
|
+
hideSkeleton.value = shouldHideSkeleton
|
|
611
|
+
|
|
307
612
|
const { data: response, error } = await promiseHandler(
|
|
308
613
|
axios.get(`${props.columnUrl}/${headerKey}`, {
|
|
614
|
+
signal: abortController.signal,
|
|
309
615
|
params: {
|
|
310
616
|
...props.columnParams,
|
|
311
617
|
limit,
|
|
@@ -314,19 +620,35 @@ async function fetchColumn (header) {
|
|
|
314
620
|
}),
|
|
315
621
|
{
|
|
316
622
|
onLoading: value => {
|
|
317
|
-
|
|
623
|
+
/**
|
|
624
|
+
* Só atualiza o loading se ainda for a request atual desta coluna,
|
|
625
|
+
* evitando que o onLoading(false) de uma request cancelada sobrescreva o da nova.
|
|
626
|
+
*/
|
|
627
|
+
if (columnFetchGenerations[headerKey] === generation) {
|
|
628
|
+
columnsLoading.value[headerKey] = value
|
|
629
|
+
}
|
|
318
630
|
},
|
|
319
|
-
useLoading: false
|
|
320
|
-
errorMessage: 'Não conseguimos buscar as colunas do board. Por favor, tente novamente em alguns minutos.'
|
|
631
|
+
useLoading: false
|
|
321
632
|
}
|
|
322
633
|
)
|
|
323
634
|
|
|
635
|
+
delete columnAbortControllers[headerKey]
|
|
636
|
+
|
|
637
|
+
hideSkeleton.value = false
|
|
638
|
+
|
|
324
639
|
if (error) {
|
|
640
|
+
// Request cancelada intencionalmente (novo fetchColumnsValues chamado): ignora silenciosamente.
|
|
641
|
+
if (isCancelledError(error)) return
|
|
642
|
+
|
|
325
643
|
emit('fetch-column-error', error)
|
|
326
644
|
|
|
645
|
+
columnsWithError.value[headerKey] = true
|
|
646
|
+
|
|
327
647
|
throw error
|
|
328
648
|
}
|
|
329
649
|
|
|
650
|
+
columnsWithError.value[headerKey] = false
|
|
651
|
+
|
|
330
652
|
const newValues = response.data?.results || []
|
|
331
653
|
const resultsModel = columnsResultsModel.value[headerKey] || []
|
|
332
654
|
|
|
@@ -346,7 +668,7 @@ async function fetchColumn (header) {
|
|
|
346
668
|
* onde cada item do objeto é uma coluna no board. O mesmo vale para "columnsFieldsModel", "columnsLoading" e
|
|
347
669
|
* "columnPagination", organizando os fields, loadings e o controle de paginação por chave identificadora do header.
|
|
348
670
|
*/
|
|
349
|
-
columnsResultsModel.value[headerKey] =
|
|
671
|
+
columnsResultsModel.value[headerKey] = hasDragAndDrop ? newColumnValues : markRaw(newColumnValues)
|
|
350
672
|
|
|
351
673
|
/*
|
|
352
674
|
* Pode acontecer das options nos fields da segunda página serem diferentes da primeira página,
|
|
@@ -364,6 +686,15 @@ async function fetchColumn (header) {
|
|
|
364
686
|
emit('fetch-column-success', { response, header })
|
|
365
687
|
}
|
|
366
688
|
|
|
689
|
+
function refreshColumn (header) {
|
|
690
|
+
const headerKey = getKeyByHeader(header)
|
|
691
|
+
|
|
692
|
+
columnsResultsModel.value[headerKey] = []
|
|
693
|
+
columnsPagination.value[headerKey] = { limit: props.limitPerColumn, offset: 0 }
|
|
694
|
+
|
|
695
|
+
fetchColumn(header)
|
|
696
|
+
}
|
|
697
|
+
|
|
367
698
|
/*
|
|
368
699
|
* Mergeia os options antigos com os novos de cada field.
|
|
369
700
|
*/
|
|
@@ -413,7 +744,7 @@ function getColumnItemById (id) {
|
|
|
413
744
|
* @returns {Object} // { date: '2024-02-15'... }
|
|
414
745
|
*/
|
|
415
746
|
function getHeaderById (id) {
|
|
416
|
-
return
|
|
747
|
+
return normalizedHeaders.value.find(header => String(getKeyByHeader(header)) === String(id))
|
|
417
748
|
}
|
|
418
749
|
|
|
419
750
|
/**
|
|
@@ -440,15 +771,28 @@ function setColumnsPagination () {
|
|
|
440
771
|
columnsPagination.value = {}
|
|
441
772
|
columnsLoading.value = {}
|
|
442
773
|
|
|
443
|
-
|
|
774
|
+
normalizedHeaders.value.forEach(header => {
|
|
444
775
|
const headerKey = getKeyByHeader(header)
|
|
445
776
|
|
|
446
777
|
columnsPagination.value[headerKey] = { limit: props.limitPerColumn, offset: 0 }
|
|
447
|
-
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Inicia como true para exibir skeleton imediatamente, evitando flash de tela vazia
|
|
781
|
+
* entre o reset e o início das novas requests.
|
|
782
|
+
*/
|
|
783
|
+
columnsLoading.value[headerKey] = true
|
|
448
784
|
})
|
|
449
785
|
}
|
|
450
786
|
|
|
451
787
|
function fetchColumnsValues () {
|
|
788
|
+
/**
|
|
789
|
+
* Cancela todas as requests em andamento antes de iniciar novas, evitando resposta de requests
|
|
790
|
+
* antigas sobrescrevendo dados da nova sessão.
|
|
791
|
+
*/
|
|
792
|
+
abortAllColumnRequests()
|
|
793
|
+
|
|
794
|
+
lazyLoadingKey.value++
|
|
795
|
+
|
|
452
796
|
reset()
|
|
453
797
|
setColumnHeightContainer()
|
|
454
798
|
setColumnsPagination()
|
|
@@ -456,16 +800,22 @@ function fetchColumnsValues () {
|
|
|
456
800
|
}
|
|
457
801
|
|
|
458
802
|
/**
|
|
459
|
-
*
|
|
803
|
+
* Descrição:
|
|
460
804
|
* Exibe o texto quando:
|
|
461
|
-
* -
|
|
462
|
-
* -
|
|
463
|
-
* -
|
|
805
|
+
* - Não está carregando a coluna
|
|
806
|
+
* - Não tem itens na coluna
|
|
807
|
+
* - Não estou fazendo o drag and drop
|
|
808
|
+
* - Não está exibindo o dialog de confirmação
|
|
464
809
|
*
|
|
465
810
|
* @param {Object} header
|
|
466
811
|
*/
|
|
467
812
|
function hasEmptyResultText (header) {
|
|
468
|
-
|
|
813
|
+
if (props.skeleton) return false
|
|
814
|
+
|
|
815
|
+
const headerKey = getKeyByHeader(header)
|
|
816
|
+
const items = getItemsByHeader(header)
|
|
817
|
+
|
|
818
|
+
return !columnsLoading.value[headerKey] && !items?.length && !isDragging.value && !showConfirmDialog.value
|
|
469
819
|
}
|
|
470
820
|
|
|
471
821
|
/*
|
|
@@ -476,7 +826,7 @@ function hasSeeMore (header) {
|
|
|
476
826
|
const headerKey = getKeyByHeader(header)
|
|
477
827
|
const hasMorePagination = columnsResultsModel.value[headerKey]?.length < columnsPagination.value[headerKey]?.count
|
|
478
828
|
|
|
479
|
-
return hasMorePagination
|
|
829
|
+
return hasMorePagination
|
|
480
830
|
}
|
|
481
831
|
|
|
482
832
|
function reset () {
|
|
@@ -495,8 +845,11 @@ function getFieldsByHeader (header) {
|
|
|
495
845
|
* Loopa todos os itens da coluna com base no ref para pegar o elemento HTML e setar e instaciar o sortable.
|
|
496
846
|
*/
|
|
497
847
|
function handleElementsList () {
|
|
498
|
-
|
|
499
|
-
|
|
848
|
+
columnContainerElements.value.forEach((columnElement, index) => {
|
|
849
|
+
// não adiciona os elementos com erro para o drag and drop.
|
|
850
|
+
if (columnElement.dataset.hasError === 'true') return
|
|
851
|
+
|
|
852
|
+
const sortable = setSortable(columnElement, index)
|
|
500
853
|
|
|
501
854
|
sortableInstances.value.push(sortable)
|
|
502
855
|
})
|
|
@@ -507,15 +860,23 @@ function handleElementsList () {
|
|
|
507
860
|
* Seta a instancia do sortable, no qual varia de acordo com as props passadas.
|
|
508
861
|
*
|
|
509
862
|
* @param {HTMLElement} element
|
|
510
|
-
* @param {
|
|
863
|
+
* @param {number} index
|
|
511
864
|
*/
|
|
512
865
|
function setSortable (element, index) {
|
|
513
866
|
const defaultSortableConfig = {
|
|
514
|
-
animation:
|
|
867
|
+
animation: 150,
|
|
868
|
+
group: 'shared',
|
|
869
|
+
ghostClass: 'qas-board-generator__ghost',
|
|
515
870
|
swapThreshold: 1,
|
|
516
|
-
delay:
|
|
871
|
+
delay: 0,
|
|
517
872
|
delayOnTouchOnly: true,
|
|
518
|
-
emptyInsertThreshold: 0
|
|
873
|
+
emptyInsertThreshold: 0,
|
|
874
|
+
filter: '[data-disable-drag="true"]',
|
|
875
|
+
scrollSensitivity: HORIZONTAL_SCROLL_SENSITIVITY,
|
|
876
|
+
scrollSpeed: HORIZONTAL_SCROLL_SPEED,
|
|
877
|
+
bubbleScroll: true,
|
|
878
|
+
forceAutoScrollFallback: true,
|
|
879
|
+
scrollFn: handleSortableScroll
|
|
519
880
|
}
|
|
520
881
|
|
|
521
882
|
/**
|
|
@@ -523,31 +884,223 @@ function setSortable (element, index) {
|
|
|
523
884
|
*/
|
|
524
885
|
const useOnlyDragAndDropY = !!props.useDragAndDropY && !props.useDragAndDropX
|
|
525
886
|
|
|
526
|
-
|
|
527
|
-
|
|
887
|
+
/**
|
|
888
|
+
* Flag local por instância do Sortable para rastrear se o drop foi tratado
|
|
889
|
+
* pelo onAdd/onSort. Isso permite que o onEnd saiba se deve chamar stopDragging
|
|
890
|
+
* (caso o usuário arraste e devolva o card sem mover para outra coluna).
|
|
891
|
+
*/
|
|
892
|
+
let dropHandled = false
|
|
528
893
|
|
|
894
|
+
const sortable = new Sortable(element, {
|
|
529
895
|
...defaultSortableConfig,
|
|
530
896
|
|
|
531
897
|
...props.sortableConfig,
|
|
532
898
|
|
|
899
|
+
/**
|
|
900
|
+
* `sort` deve vir APÓS os spreads para não ser sobrescrito pelo defaultSortableConfig.
|
|
901
|
+
* Quando useDragAndDropY é true, o sort precisa estar habilitado para que o SortableJS
|
|
902
|
+
* rastreie ativamente a posição do item dentro da coluna durante o drag. Sem isso,
|
|
903
|
+
* o item oscila entre colunas porque o Sortable do destino não "assume" o controle do item.
|
|
904
|
+
*/
|
|
905
|
+
sort: props.useDragAndDropY,
|
|
906
|
+
|
|
533
907
|
group: useOnlyDragAndDropY ? `column-${index}` : 'shared',
|
|
534
908
|
|
|
535
|
-
|
|
909
|
+
/**
|
|
910
|
+
* invertSwap muda o algoritmo de swap: em vez de trocar quando o centro do item arrastado
|
|
911
|
+
* cruza o centro do alvo (instável, causa oscilação), troca apenas quando cruza a BORDA
|
|
912
|
+
* superior/inferior do alvo. Isso cria uma "zona morta" que elimina o jitter.
|
|
913
|
+
*/
|
|
914
|
+
invertSwap: true,
|
|
915
|
+
|
|
916
|
+
onStart: () => {
|
|
917
|
+
dropHandled = false
|
|
918
|
+
startDragging()
|
|
919
|
+
},
|
|
536
920
|
|
|
537
|
-
|
|
921
|
+
onAdd: event => {
|
|
922
|
+
dropHandled = true
|
|
923
|
+
onDropCard(event)
|
|
924
|
+
},
|
|
538
925
|
|
|
539
|
-
|
|
926
|
+
/**
|
|
927
|
+
* Chamado sempre que o drag termina, independente do resultado.
|
|
928
|
+
* Só chama stopDragging se onAdd/onSort não trataram o drop,
|
|
929
|
+
* ou seja, o usuário abandonou o drag sem mover o card.
|
|
930
|
+
*/
|
|
931
|
+
onEnd: () => {
|
|
932
|
+
if (!dropHandled) stopDragging()
|
|
933
|
+
},
|
|
540
934
|
|
|
541
935
|
...(props.useDragAndDropY && {
|
|
542
|
-
onSort: event =>
|
|
936
|
+
onSort: event => {
|
|
937
|
+
dropHandled = true
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* O SortableJS dispara onSort em AMBAS as listas envolvidas quando um item
|
|
941
|
+
* é arrastado entre colunas (cross-column): na lista de origem porque perdeu
|
|
942
|
+
* um item e na lista de destino porque ganhou um.
|
|
943
|
+
* O onAdd já trata o cross-column na lista de destino, então ao ignorar o
|
|
944
|
+
* onSort para eventos cross-column evitamos processar o mesmo drop duas vezes,
|
|
945
|
+
* o que causava corrupção do model (item duplicado no destino e remoção
|
|
946
|
+
* incorreta do último item da origem via splice(-1, 1)).
|
|
947
|
+
*/
|
|
948
|
+
if (event.from !== event.to) return
|
|
949
|
+
|
|
950
|
+
onDropCard(event)
|
|
951
|
+
}
|
|
543
952
|
})
|
|
544
953
|
})
|
|
545
954
|
|
|
546
955
|
return sortable
|
|
547
956
|
}
|
|
548
957
|
|
|
549
|
-
function
|
|
550
|
-
|
|
958
|
+
function startDragging () {
|
|
959
|
+
draggingCount.value++
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function stopDragging () {
|
|
963
|
+
draggingCount.value = Math.max(0, draggingCount.value - 1)
|
|
964
|
+
resetSmoothScroll()
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Intercepta cada chamada de scroll do SortableJS.
|
|
969
|
+
* - Scroll vertical (colunas): retorna 'continue' para manter o comportamento nativo.
|
|
970
|
+
* - Scroll horizontal (container do board): aplica smooth scroll interpolado via rAF.
|
|
971
|
+
*
|
|
972
|
+
* rAF (requestAnimationFrame) sincroniza com o refresh da tela (~60fps),
|
|
973
|
+
* tornando o scroll suave. Sem rAF, seria um "salto" abrupto.
|
|
974
|
+
*/
|
|
975
|
+
function handleSortableScroll (offsetX, offsetY, evt, _touchEvt, scrollEl) {
|
|
976
|
+
// Obtém o container com scroll horizontal (`.qas-grabbable__container`).
|
|
977
|
+
const horizontalContainer = columnContainerElements.value[0]?.closest('.qas-grabbable__container')
|
|
978
|
+
|
|
979
|
+
// Acumula o offset no target e inicia a interpolação via rAF.
|
|
980
|
+
if (scrollEl === horizontalContainer && offsetX) {
|
|
981
|
+
if (smoothScrollEl !== scrollEl) {
|
|
982
|
+
smoothScrollTarget = scrollEl.scrollLeft
|
|
983
|
+
smoothScrollEl = scrollEl
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const maxScroll = scrollEl.scrollWidth - scrollEl.clientWidth
|
|
987
|
+
|
|
988
|
+
smoothScrollTarget = Math.max(0, Math.min(smoothScrollTarget + offsetX, maxScroll))
|
|
989
|
+
|
|
990
|
+
// Se não há animação em andamento, inicia uma.
|
|
991
|
+
// rAF (requestAnimationFrame) agenda a função smoothScrollStep para o próximo frame (~16ms a 60fps).
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Se não há animação em andamento, inicia uma. rAF (requestAnimationFrame) agenda a função smoothScrollStep
|
|
995
|
+
* para o próximo frame (~16ms a 60fps).
|
|
996
|
+
*/
|
|
997
|
+
if (!smoothScrollRafId) {
|
|
998
|
+
smoothScrollRafId = requestAnimationFrame(smoothScrollStep)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* SortableJS chama essa função com offsets de scroll.
|
|
1006
|
+
* Para scroll vertical: retorna 'continue' para deixar o comportamento nativo.
|
|
1007
|
+
* Para scroll horizontal: aplica smooth scroll customizado via rAF.
|
|
1008
|
+
*/
|
|
1009
|
+
if (offsetY) {
|
|
1010
|
+
const clientY = evt?.clientY ?? evt?.touches?.[0]?.clientY
|
|
1011
|
+
|
|
1012
|
+
if (clientY !== undefined) {
|
|
1013
|
+
const rect = scrollEl.getBoundingClientRect()
|
|
1014
|
+
const distFromEdge = Math.min(clientY - rect.top, rect.bottom - clientY)
|
|
1015
|
+
|
|
1016
|
+
if (distFromEdge > VERTICAL_SCROLL_SENSITIVITY) return
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
scrollEl.scrollTop += offsetY * (VERTICAL_SCROLL_SPEED / HORIZONTAL_SCROLL_SPEED)
|
|
1020
|
+
return
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return 'continue'
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Loop de animação que interpola o scrollLeft em direção ao target
|
|
1028
|
+
* usando fator de lerp (move 20% da distância restante por frame).
|
|
1029
|
+
*
|
|
1030
|
+
* Funciona assim:
|
|
1031
|
+
* 1. Calcula a diferença entre a posição atual e a posição desejada (target).
|
|
1032
|
+
* 2. Se ainda há diferença significativa, move 20% da distância restante.
|
|
1033
|
+
* 3. Agenda o próximo frame via rAF para continuar a interpolação.
|
|
1034
|
+
* 4. Quando chega perto o suficiente (~0.5px), para a animação.
|
|
1035
|
+
*
|
|
1036
|
+
* Resultado: scroll suave e progressivo, não um "salto" abrupto.
|
|
1037
|
+
*/
|
|
1038
|
+
function smoothScrollStep () {
|
|
1039
|
+
smoothScrollRafId = null
|
|
1040
|
+
|
|
1041
|
+
if (!smoothScrollEl) return
|
|
1042
|
+
|
|
1043
|
+
const current = smoothScrollEl.scrollLeft
|
|
1044
|
+
const diff = smoothScrollTarget - current
|
|
1045
|
+
|
|
1046
|
+
// Se chegou bem perto do target, para na posição exata.
|
|
1047
|
+
if (Math.abs(diff) < 0.5) {
|
|
1048
|
+
smoothScrollEl.scrollLeft = smoothScrollTarget
|
|
1049
|
+
return
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Move 20% da distância restante (interpolação suave).
|
|
1053
|
+
smoothScrollEl.scrollLeft = current + diff * SMOOTH_SCROLL_LERP
|
|
1054
|
+
|
|
1055
|
+
// Agenda o próximo frame para continuar a animação (chamada recursiva via rAF).
|
|
1056
|
+
smoothScrollRafId = requestAnimationFrame(smoothScrollStep)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function resetSmoothScroll () {
|
|
1060
|
+
// Cancela a animação agendada (rAF) para evitar que continue executando após o drag terminar.
|
|
1061
|
+
if (smoothScrollRafId) {
|
|
1062
|
+
cancelAnimationFrame(smoothScrollRafId)
|
|
1063
|
+
smoothScrollRafId = null
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
smoothScrollEl = null
|
|
1067
|
+
smoothScrollTarget = 0
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Reverte a manipulação de DOM feita pelo SortableJS.
|
|
1072
|
+
* Deve ser chamada ANTES de qualquer atualização reativa do model,
|
|
1073
|
+
* para que o Vue parta de um estado DOM consistente ao re-renderizar.
|
|
1074
|
+
*/
|
|
1075
|
+
function revertDomDrag (event) {
|
|
1076
|
+
if (props.useDragAndDropX) {
|
|
1077
|
+
event.from.insertBefore(event.item, event.from.children[event.oldIndex] || null)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (props.useDragAndDropY) {
|
|
1081
|
+
const oldIndex = event.oldIndex
|
|
1082
|
+
const targetIndex = oldIndex === 0 ? oldIndex : oldIndex + 1
|
|
1083
|
+
const insertBeforeElement = targetIndex < event.from.children.length
|
|
1084
|
+
? event.from.children[targetIndex]
|
|
1085
|
+
: null
|
|
1086
|
+
|
|
1087
|
+
event.from.insertBefore(event.item, insertBeforeElement)
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Adiciona um item na lista de resultados de uma coluna, criando uma nova
|
|
1093
|
+
* referência de array para garantir reatividade com markRaw.
|
|
1094
|
+
*/
|
|
1095
|
+
function addItemToList ({ headerKey, item, index }) {
|
|
1096
|
+
const columnItemList = columnsResultsModel.value[headerKey]
|
|
1097
|
+
const updatedList = [...columnItemList]
|
|
1098
|
+
const insertIndex = Math.min(index, updatedList.length)
|
|
1099
|
+
|
|
1100
|
+
updatedList.splice(insertIndex, 0, item)
|
|
1101
|
+
|
|
1102
|
+
columnsResultsModel.value[headerKey] = updatedList
|
|
1103
|
+
columnsPagination.value[headerKey].count += 1
|
|
551
1104
|
}
|
|
552
1105
|
|
|
553
1106
|
function onDropCard (event) {
|
|
@@ -569,7 +1122,7 @@ function onDropCard (event) {
|
|
|
569
1122
|
return
|
|
570
1123
|
}
|
|
571
1124
|
|
|
572
|
-
|
|
1125
|
+
props.useConfirmDialog
|
|
573
1126
|
? openConfirmDialog()
|
|
574
1127
|
: confirmDrop(event)
|
|
575
1128
|
}
|
|
@@ -586,34 +1139,11 @@ function closeConfirmDialog () {
|
|
|
586
1139
|
* @param {event} event
|
|
587
1140
|
*/
|
|
588
1141
|
function cancelDrop (event) {
|
|
589
|
-
|
|
590
|
-
* Insere na posição antiga que pertencia (event.oldIndex) dentro do seu antigo pai (event.from)
|
|
591
|
-
*/
|
|
592
|
-
if (props.useDragAndDropX) event.from.insertBefore(event.item, event.from.children[event.oldIndex])
|
|
593
|
-
|
|
594
|
-
if (props.useDragAndDropY) {
|
|
595
|
-
const oldIndex = event.oldIndex
|
|
1142
|
+
revertDomDrag(event)
|
|
596
1143
|
|
|
597
|
-
|
|
598
|
-
* 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.
|
|
599
|
-
*
|
|
600
|
-
* 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.
|
|
601
|
-
*/
|
|
602
|
-
const targetIndex = oldIndex === 0 ? oldIndex : oldIndex + 1
|
|
1144
|
+
if (props.useConfirmDialog) closeConfirmDialog()
|
|
603
1145
|
|
|
604
|
-
|
|
605
|
-
* Verifica se o índice alvo é válido, caso contrário, define como o final
|
|
606
|
-
*/
|
|
607
|
-
const insertBeforeElement = targetIndex < event.from.children.length
|
|
608
|
-
? event.from.children[targetIndex]
|
|
609
|
-
: null
|
|
610
|
-
|
|
611
|
-
event.from.insertBefore(event.item, insertBeforeElement)
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
if (hasConfirmDialogProps.value) closeConfirmDialog()
|
|
615
|
-
|
|
616
|
-
toggleIsDragging()
|
|
1146
|
+
stopDragging()
|
|
617
1147
|
}
|
|
618
1148
|
|
|
619
1149
|
function confirmDrop (event) {
|
|
@@ -622,7 +1152,47 @@ function confirmDrop (event) {
|
|
|
622
1152
|
const { headerKey: newHeaderKey } = to.dataset
|
|
623
1153
|
const { headerKey: oldHeaderKey } = from.dataset
|
|
624
1154
|
|
|
625
|
-
|
|
1155
|
+
/**
|
|
1156
|
+
* 1. Reverte o DOM imediatamente para que o Vue parta de um estado consistente.
|
|
1157
|
+
* O SortableJS já moveu o elemento fisicamente, mas precisamos devolvê-lo
|
|
1158
|
+
* antes de atualizar o model reativo.
|
|
1159
|
+
*/
|
|
1160
|
+
revertDomDrag(event)
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* 2. Atualização otimista: move o item no model ANTES da request de API.
|
|
1164
|
+
* Isso garante que a UI esteja sempre sincronizada com o model,
|
|
1165
|
+
* eliminando race conditions entre múltiplos drops rápidos.
|
|
1166
|
+
*/
|
|
1167
|
+
const item = getColumnItemById(itemId)
|
|
1168
|
+
|
|
1169
|
+
removeItemFromList({ headerKey: oldHeaderKey, itemId })
|
|
1170
|
+
addItemToList({ headerKey: newHeaderKey, item, index: event.newIndex })
|
|
1171
|
+
|
|
1172
|
+
stopDragging()
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* 3. Marca o item como "em atualização" imediatamente, antes mesmo
|
|
1176
|
+
* da request de API iniciar (pode estar na fila).
|
|
1177
|
+
*/
|
|
1178
|
+
updatingPositionItemKeys.value = new Set([...updatingPositionItemKeys.value, itemId])
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* 4. Enfileira a chamada de API para confirmação no servidor.
|
|
1182
|
+
* Se falhar, o model é revertido automaticamente.
|
|
1183
|
+
*/
|
|
1184
|
+
enqueueUpdatePosition({ newHeaderKey, oldHeaderKey, itemId, event, optimisticItem: item })
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Enfileira a chamada de updatePosition para execução sequencial.
|
|
1189
|
+
* Garante que a próxima chamada só inicia após a anterior ter finalizado,
|
|
1190
|
+
* evitando race conditions no estado compartilhado.
|
|
1191
|
+
*/
|
|
1192
|
+
function enqueueUpdatePosition (params) {
|
|
1193
|
+
updatePositionQueue = updatePositionQueue.then(
|
|
1194
|
+
() => updatePosition(params)
|
|
1195
|
+
)
|
|
626
1196
|
}
|
|
627
1197
|
|
|
628
1198
|
/**
|
|
@@ -644,9 +1214,13 @@ function removeItemFromList ({ headerKey, itemId }) {
|
|
|
644
1214
|
const itemIndex = columnItemList.findIndex(itemContent => itemContent[props.itemIdKey] === itemId)
|
|
645
1215
|
|
|
646
1216
|
/**
|
|
647
|
-
*
|
|
1217
|
+
* Cria uma nova referência de array para garantir que o Vue detecte a mudança de estado,
|
|
1218
|
+
* mesmo quando o array é marcado com markRaw (que não rastreia mutações in-place).
|
|
648
1219
|
*/
|
|
649
|
-
columnItemList
|
|
1220
|
+
const updatedList = [...columnItemList]
|
|
1221
|
+
updatedList.splice(itemIndex, 1)
|
|
1222
|
+
|
|
1223
|
+
columnsResultsModel.value[headerKey] = updatedList
|
|
650
1224
|
|
|
651
1225
|
/**
|
|
652
1226
|
* Remove o item do count da coluna para não mostrar o botão de "Ver mais¨.
|
|
@@ -654,6 +1228,32 @@ function removeItemFromList ({ headerKey, itemId }) {
|
|
|
654
1228
|
columnsPagination.value[headerKey].count -= 1
|
|
655
1229
|
}
|
|
656
1230
|
|
|
1231
|
+
/**
|
|
1232
|
+
* Substitui um item na lista de uma coluna pelos dados atualizados do servidor.
|
|
1233
|
+
*
|
|
1234
|
+
* 1. Localiza a lista de itens da coluna
|
|
1235
|
+
* 2. Encontra o índice do item usando o itemIdKey como identificador
|
|
1236
|
+
* 3. Se encontrado, altera o item localmente (sem criar nova referência de array)
|
|
1237
|
+
*
|
|
1238
|
+
* @param {Object} params
|
|
1239
|
+
* @param {string} params.headerKey - ID da coluna
|
|
1240
|
+
* @param {string|number} params.itemId - ID do item a atualizar (deve corresponder a props.itemIdKey)
|
|
1241
|
+
* @param {Object} params.updatedItem - Novo objeto do item com dados atualizados
|
|
1242
|
+
*
|
|
1243
|
+
*/
|
|
1244
|
+
function updateItemInList ({ headerKey, itemId, updatedItem }) {
|
|
1245
|
+
const columnItemList = columnsResultsModel.value[headerKey]
|
|
1246
|
+
|
|
1247
|
+
// Encontra o índice do item na lista usando o identificador (itemIdKey)
|
|
1248
|
+
const itemIndex = columnItemList.findIndex(itemContent => itemContent[props.itemIdKey] === itemId)
|
|
1249
|
+
|
|
1250
|
+
// Se não encontrar (itemIndex === -1), retorna sem fazer nada
|
|
1251
|
+
if (!~itemIndex) return
|
|
1252
|
+
|
|
1253
|
+
// Substitui o item antigo pelo item atualizado na mesma posição
|
|
1254
|
+
columnItemList.splice(itemIndex, 1, updatedItem)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
657
1257
|
/**
|
|
658
1258
|
* Método que realiza a request de update
|
|
659
1259
|
*
|
|
@@ -664,15 +1264,23 @@ function removeItemFromList ({ headerKey, itemId }) {
|
|
|
664
1264
|
* event: event
|
|
665
1265
|
* }}
|
|
666
1266
|
*/
|
|
667
|
-
async function updatePosition ({ newHeaderKey, oldHeaderKey, itemId, event }) {
|
|
1267
|
+
async function updatePosition ({ newHeaderKey, oldHeaderKey, itemId, event, optimisticItem }) {
|
|
668
1268
|
const params = {
|
|
669
1269
|
[props.columnIdKey]: newHeaderKey,
|
|
670
1270
|
...(props.useDragAndDropY && { newIndex: event.newIndex }),
|
|
671
1271
|
...props.updatePositionParams
|
|
672
1272
|
}
|
|
673
1273
|
|
|
1274
|
+
const isFnUpdatePositionUrl = typeof props.updatePositionUrl === 'function'
|
|
1275
|
+
|
|
1276
|
+
hideSkeleton.value = true
|
|
1277
|
+
|
|
1278
|
+
const url = isFnUpdatePositionUrl
|
|
1279
|
+
? props.updatePositionUrl({ newHeaderKey, oldHeaderKey, itemId })
|
|
1280
|
+
: `${props.updatePositionUrl}/${itemId}/update-position`
|
|
1281
|
+
|
|
674
1282
|
const { data, error } = await promiseHandler(
|
|
675
|
-
axios.patch(
|
|
1283
|
+
axios.patch(url, params),
|
|
676
1284
|
{
|
|
677
1285
|
errorMessage: 'Ocorreu um erro ao atualizar a posição de seu item.',
|
|
678
1286
|
useLoading: false,
|
|
@@ -684,41 +1292,127 @@ async function updatePosition ({ newHeaderKey, oldHeaderKey, itemId, event }) {
|
|
|
684
1292
|
}
|
|
685
1293
|
)
|
|
686
1294
|
|
|
1295
|
+
const updatedKeys = new Set(updatingPositionItemKeys.value)
|
|
1296
|
+
updatedKeys.delete(itemId)
|
|
1297
|
+
updatingPositionItemKeys.value = updatedKeys
|
|
1298
|
+
|
|
687
1299
|
if (error) {
|
|
688
|
-
|
|
1300
|
+
/**
|
|
1301
|
+
* Reverte a atualização otimista: move o item de volta da coluna de destino
|
|
1302
|
+
* para a coluna de origem no model.
|
|
1303
|
+
*/
|
|
1304
|
+
removeItemFromList({ headerKey: newHeaderKey, itemId })
|
|
1305
|
+
addItemToList({ headerKey: oldHeaderKey, item: optimisticItem, index: event.oldIndex })
|
|
689
1306
|
|
|
690
1307
|
emit('update-error', error)
|
|
691
1308
|
|
|
692
1309
|
return
|
|
693
1310
|
}
|
|
694
1311
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1312
|
+
/**
|
|
1313
|
+
* Atualiza o item na coluna de destino com os dados retornados pelo servidor,
|
|
1314
|
+
* que podem conter campos atualizados (timestamps, status, etc).
|
|
1315
|
+
*/
|
|
1316
|
+
if (data.data?.result) {
|
|
1317
|
+
updateItemInList({ headerKey: newHeaderKey, itemId, updatedItem: data.data.result })
|
|
1318
|
+
}
|
|
698
1319
|
|
|
699
1320
|
isUpdatingPosition.value = true
|
|
700
1321
|
|
|
701
|
-
toggleIsDragging()
|
|
702
|
-
|
|
703
1322
|
closeConfirmDialog()
|
|
704
1323
|
|
|
705
1324
|
emit('update-success', data.data)
|
|
706
1325
|
}
|
|
707
1326
|
|
|
708
|
-
function
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1327
|
+
function destroySortable () {
|
|
1328
|
+
sortableInstances.value.forEach(sortable => sortable.destroy())
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Cancela a request em andamento de uma coluna específica.
|
|
1333
|
+
*/
|
|
1334
|
+
function abortColumnRequest (headerKey) {
|
|
1335
|
+
columnAbortControllers[headerKey]?.abort()
|
|
1336
|
+
delete columnAbortControllers[headerKey]
|
|
1337
|
+
}
|
|
713
1338
|
|
|
1339
|
+
/**
|
|
1340
|
+
* Cancela todas as requests de colunas em andamento.
|
|
1341
|
+
* Chamado em onBeforeUnmount e no início de fetchColumnsValues.
|
|
1342
|
+
*/
|
|
1343
|
+
function abortAllColumnRequests () {
|
|
714
1344
|
/**
|
|
715
|
-
*
|
|
1345
|
+
* Invalida a sessão atual de fetchColumns para que, após as requests visíveis
|
|
1346
|
+
* serem canceladas, a verificação de sessão impeça o início das requests das
|
|
1347
|
+
* colunas ocultas (que ainda não haviam sido disparadas).
|
|
1348
|
+
* Sem isso, fetchColumns passaria na verificação e iniciaria as hidden columns
|
|
1349
|
+
* mesmo após o componente ter iniciado o processo de unmount.
|
|
716
1350
|
*/
|
|
717
|
-
|
|
1351
|
+
currentFetchColumnsSession++
|
|
1352
|
+
|
|
1353
|
+
Object.keys(columnAbortControllers).forEach(key => {
|
|
1354
|
+
columnAbortControllers[key]?.abort()
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
columnAbortControllers = {}
|
|
718
1358
|
}
|
|
719
1359
|
|
|
720
|
-
|
|
721
|
-
|
|
1360
|
+
/**
|
|
1361
|
+
* Verifica se o erro é resultado de um cancelamento intencional de request
|
|
1362
|
+
* (AbortController.abort() via axios ou fetch).
|
|
1363
|
+
*/
|
|
1364
|
+
function isCancelledError (error) {
|
|
1365
|
+
return error?.code === 'ERR_CANCELED' || error?.name === 'CanceledError' || error?.name === 'AbortError'
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Transfere um item de uma coluna para outra localmente, sem realizar nenhuma requisição.
|
|
1370
|
+
* O item é sempre inserido como primeiro elemento na coluna de destino.
|
|
1371
|
+
* Após a transferência, o SortableJS continua funcionando normalmente sobre os elementos
|
|
1372
|
+
* do DOM atualizados pelo Vue, sem necessidade de reinicialização.
|
|
1373
|
+
*
|
|
1374
|
+
* @param {Object} params
|
|
1375
|
+
* @param {string|number} params.itemId - ID do item a ser movido (valor correspondente à prop `itemIdKey`).
|
|
1376
|
+
* @param {string|number} params.fromColumnId - ID da coluna de origem (valor correspondente à prop `columnIdKey`).
|
|
1377
|
+
* @param {string|number} params.toColumnId - ID da coluna de destino (valor correspondente à prop `columnIdKey`).
|
|
1378
|
+
* @param {Object} [params.updatedItem] - Dados atualizados do item. Quando informado, sobrescreve o item original na coluna de destino.
|
|
1379
|
+
* @returns {void}
|
|
1380
|
+
*/
|
|
1381
|
+
function transferItemToColumn ({ itemId, fromColumnId, toColumnId, updatedItem }) {
|
|
1382
|
+
const sourceColumn = columnsResultsModel.value[fromColumnId]
|
|
1383
|
+
const itemIndex = sourceColumn?.findIndex(item => item[props.itemIdKey] === itemId)
|
|
1384
|
+
|
|
1385
|
+
if (!sourceColumn || itemIndex === -1) return
|
|
1386
|
+
|
|
1387
|
+
const item = updatedItem ?? sourceColumn[itemIndex]
|
|
1388
|
+
|
|
1389
|
+
removeItemFromList({ headerKey: fromColumnId, itemId })
|
|
1390
|
+
|
|
1391
|
+
columnsResultsModel.value[toColumnId].splice(0, 0, item)
|
|
1392
|
+
columnsPagination.value[toColumnId].count += 1
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function hasSkeletonByHeader (header) {
|
|
1396
|
+
const headerKey = getKeyByHeader(header)
|
|
1397
|
+
|
|
1398
|
+
return (props.skeleton || columnsLoading.value[headerKey]) && !hideSkeleton.value
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function getColumnClasses (header) {
|
|
1402
|
+
const headerKey = getKeyByHeader(header)
|
|
1403
|
+
|
|
1404
|
+
return {
|
|
1405
|
+
'qas-board-generator__column-error': columnsWithError.value[headerKey]
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function getCardsContainerProps (header) {
|
|
1410
|
+
const headerKey = getKeyByHeader(header)
|
|
1411
|
+
|
|
1412
|
+
return {
|
|
1413
|
+
'data-header-key': headerKey,
|
|
1414
|
+
'data-has-error': columnsWithError.value[headerKey]
|
|
1415
|
+
}
|
|
722
1416
|
}
|
|
723
1417
|
</script>
|
|
724
1418
|
|
|
@@ -747,5 +1441,32 @@ function destroySortable () {
|
|
|
747
1441
|
&__column-items {
|
|
748
1442
|
height: calc(100% - 60px);
|
|
749
1443
|
}
|
|
1444
|
+
|
|
1445
|
+
&__column-error {
|
|
1446
|
+
border: 1px solid $negative;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
&__ghost {
|
|
1450
|
+
border: 1px solid $grey-4 !important;
|
|
1451
|
+
overflow: hidden;
|
|
1452
|
+
border-radius: $generic-border-radius;
|
|
1453
|
+
position: relative;
|
|
1454
|
+
margin-bottom: var(--qas-spacing-sm);
|
|
1455
|
+
|
|
1456
|
+
&::after {
|
|
1457
|
+
align-items: center;
|
|
1458
|
+
background-color: $grey-3;
|
|
1459
|
+
border-radius: inherit;
|
|
1460
|
+
color: $grey-8;
|
|
1461
|
+
content: 'place_item';
|
|
1462
|
+
display: flex;
|
|
1463
|
+
font-family: 'Material Symbols Rounded';
|
|
1464
|
+
font-size: 40px;
|
|
1465
|
+
inset: 0;
|
|
1466
|
+
justify-content: center;
|
|
1467
|
+
position: absolute;
|
|
1468
|
+
z-index: 999;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
750
1471
|
}
|
|
751
1472
|
</style>
|