@bildvitta/quasar-ui-asteroid 3.20.0-beta.14-alpha.3 → 3.20.0-beta.14
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 +2 -2
- package/src/components/actions-menu/QasActionsMenu.vue +4 -4
- package/src/components/board-generator/QasBoardGenerator.vue +321 -100
- package/src/components/board-generator/QasBoardGenerator.yml +56 -2
- package/src/components/board-generator/private/PvBoardGeneratorCardsContainer.vue +0 -1
- package/src/components/box/QasBox.yml +5 -0
- package/src/components/card/QasCard.vue +1 -0
- package/src/components/date-time-input/QasDateTimeInput.vue +30 -6
- package/src/components/dialog/QasDialog.vue +1 -3
- package/src/components/dialog/QasDialog.yml +8 -0
- package/src/components/expansion-item/QasExpansionItem.yml +5 -0
- package/src/components/form-generator/QasFormGenerator.yml +5 -0
- package/src/components/header/QasHeader.yml +5 -0
- package/src/components/lazy-loading-components/QasLazyLoadingComponents.vue +0 -7
- package/src/components/lazy-loading-components/QasLazyLoadingComponents.yml +34 -9
- package/src/components/select-list-dialog/QasSelectListDialog.vue +1 -1
- package/src/components/stepper/QasStepper.vue +24 -2
- package/src/composables/use-screen.js +17 -1
- package/src/helpers/filters.js +1 -1
- package/src/helpers/set-scroll-gradient.js +5 -2
- package/src/mixins/search-filter.js +1 -1
- 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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bildvitta/quasar-ui-asteroid",
|
|
3
3
|
"description": "Asteroid",
|
|
4
|
-
"version": "3.20.0-beta.14
|
|
4
|
+
"version": "3.20.0-beta.14",
|
|
5
5
|
"author": "Bild & Vitta <systemteam@bild.com.br>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./src/asteroid.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"lodash-es": "^4.17.21",
|
|
56
56
|
"pica": "^9.0.1",
|
|
57
57
|
"signature_pad": "^4.1.5",
|
|
58
|
-
"sortablejs": "
|
|
58
|
+
"sortablejs": "1.15.7"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@quasar/extras": "^1.16.0",
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
<q-item v-bind="getItemProps(item)" :key="key" active-class="primary" clickable data-cy="actions-menu-list-item" @click="setClickHandler(item)">
|
|
7
7
|
<q-item-section avatar>
|
|
8
8
|
<q-spinner v-if="item.loading" size="sm" />
|
|
9
|
-
<q-icon v-else :class="
|
|
9
|
+
<q-icon v-else :class="getMagicAiClasses(item)" :name="item.icon" />
|
|
10
10
|
</q-item-section>
|
|
11
11
|
|
|
12
12
|
<q-item-section>
|
|
13
|
-
<q-item-label :class="
|
|
13
|
+
<q-item-label :class="getMagicAiClasses(item)">
|
|
14
14
|
{{ item.label }}
|
|
15
15
|
</q-item-label>
|
|
16
16
|
</q-item-section>
|
|
@@ -286,8 +286,8 @@ function getItemProps (item) {
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
function
|
|
290
|
-
return { 'text-magic-ai':
|
|
289
|
+
function getMagicAiClasses ({ useMagicAiColor }) {
|
|
290
|
+
return { 'text-magic-ai': useMagicAiColor }
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
function handleMenuModel (newValue, oldValue) {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
<qas-grabbable class="qas-board-generator" v-bind="grabbableProps">
|
|
4
4
|
<div ref="columnsContainer" class="no-wrap q-gutter-md q-pb-xs q-px-lg row">
|
|
5
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="
|
|
7
|
-
<div class="ellipsis q-mb-md text-grey-10"
|
|
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
8
|
<qas-skeleton v-if="props.skeleton" type="text" use-contrast width="80%" />
|
|
9
9
|
|
|
10
10
|
<slot v-else :fields="getFieldsByHeader(header)" :header="header" :index="index" name="header-column" />
|
|
@@ -22,24 +22,19 @@
|
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
<div class="text-center">
|
|
25
|
-
<qas-btn class="q-mt-md" icon="sym_r_refresh" label="Tentar novamente" :loading="columnsLoading[getKeyByHeader(header)]" @click="
|
|
25
|
+
<qas-btn class="q-mt-md" icon="sym_r_refresh" label="Tentar novamente" :loading="columnsLoading[getKeyByHeader(header)]" @click="handleFetchColumnClick(header)" />
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
|
28
28
|
|
|
29
29
|
<template v-else>
|
|
30
30
|
<qas-lazy-loading-components :threshold="0">
|
|
31
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" :item="item" name="column-item" /> -->
|
|
33
|
-
<!-- <qas-card v-if="updatingPositionItemKey === item[props.itemIdKey]" v-bind="skeletonCards.at(0)" :key="item[props.itemIdKey]" class="q-mb-sm" :column-index="index">
|
|
34
|
-
<template #default />
|
|
35
|
-
</qas-card> -->
|
|
36
|
-
|
|
37
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" />
|
|
38
33
|
</div>
|
|
39
34
|
</qas-lazy-loading-components>
|
|
40
35
|
|
|
41
36
|
<div class="full-width justify-center row">
|
|
42
|
-
<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="
|
|
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)" />
|
|
43
38
|
|
|
44
39
|
<template v-if="hasSkeletonByHeader(header)">
|
|
45
40
|
<div class="q-col-gutter-y-sm row">
|
|
@@ -117,11 +112,6 @@ const props = defineProps({
|
|
|
117
112
|
default: () => ({})
|
|
118
113
|
},
|
|
119
114
|
|
|
120
|
-
headerBoxProps: {
|
|
121
|
-
type: Object,
|
|
122
|
-
default: () => ({})
|
|
123
|
-
},
|
|
124
|
-
|
|
125
115
|
columnIdKey: {
|
|
126
116
|
type: String,
|
|
127
117
|
required: true
|
|
@@ -182,13 +172,7 @@ const props = defineProps({
|
|
|
182
172
|
},
|
|
183
173
|
|
|
184
174
|
skeleton: {
|
|
185
|
-
type: Boolean
|
|
186
|
-
default: true
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
useMarkRaw: {
|
|
190
|
-
type: Boolean,
|
|
191
|
-
default: true
|
|
175
|
+
type: Boolean
|
|
192
176
|
},
|
|
193
177
|
|
|
194
178
|
useDragAndDropX: {
|
|
@@ -247,8 +231,13 @@ const columnsLoading = ref({})
|
|
|
247
231
|
const columnsFieldsModel = ref({})
|
|
248
232
|
const showConfirmDialog = ref(false)
|
|
249
233
|
const isLoadingUpdatePosition = ref(false)
|
|
250
|
-
const
|
|
234
|
+
const hideSkeleton = ref(false)
|
|
251
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
|
+
*/
|
|
252
241
|
const updatingPositionItemKeys = ref(new Set())
|
|
253
242
|
|
|
254
243
|
/**
|
|
@@ -307,6 +296,33 @@ let columnAbortControllers = {}
|
|
|
307
296
|
*/
|
|
308
297
|
let currentFetchColumnsSession = 0
|
|
309
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
|
+
|
|
310
326
|
/**
|
|
311
327
|
* Geração por coluna para invalidar callbacks onLoading de requests canceladas.
|
|
312
328
|
* Evita que o onLoading(false) de uma request cancelada sobrescreva o estado da nova request.
|
|
@@ -328,13 +344,15 @@ const grabbableProps = {
|
|
|
328
344
|
* Gera cards de skeleton para exibir enquanto carrega os itens da coluna, o mesmo é usado para preencher a coluna
|
|
329
345
|
* quando a prop "skeleton" for true, indicando que é para simular o carregamento.
|
|
330
346
|
*/
|
|
331
|
-
const skeletonCards = Array.from({ length: 6 }).map(() =>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
347
|
+
const skeletonCards = Array.from({ length: 6 }).map(() => {
|
|
348
|
+
return {
|
|
349
|
+
skeleton: true,
|
|
350
|
+
title: '-',
|
|
351
|
+
expansionProps: { label: '-' },
|
|
352
|
+
actionsMenuProps: { list: {} },
|
|
353
|
+
useSelection: true
|
|
354
|
+
}
|
|
355
|
+
})
|
|
338
356
|
|
|
339
357
|
// computeds
|
|
340
358
|
const columnContainerElements = computed(() => {
|
|
@@ -354,38 +372,6 @@ const normalizedHeaders = computed(() => {
|
|
|
354
372
|
return props.headers
|
|
355
373
|
})
|
|
356
374
|
|
|
357
|
-
// watchers
|
|
358
|
-
watch(
|
|
359
|
-
() => normalizedHeaders.value,
|
|
360
|
-
value => {
|
|
361
|
-
if (isLoadingUpdatePosition.value || !value?.length) return
|
|
362
|
-
|
|
363
|
-
fetchColumnsValues()
|
|
364
|
-
}
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
watch(() => columnContainerElements.value, () => {
|
|
368
|
-
setColumnHeightContainer()
|
|
369
|
-
handleElementsList()
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
// hooks
|
|
373
|
-
onMounted(() => {
|
|
374
|
-
if (normalizedHeaders.value.length) {
|
|
375
|
-
fetchColumnsValues()
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
window.addEventListener('resize', setColumnHeightContainer)
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
onBeforeUnmount(() => {
|
|
382
|
-
abortAllColumnRequests()
|
|
383
|
-
destroySortable()
|
|
384
|
-
|
|
385
|
-
window.removeEventListener('resize', setColumnHeightContainer)
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
// Computeds
|
|
389
375
|
const columnsResultsModel = computed({
|
|
390
376
|
get () {
|
|
391
377
|
return props.results
|
|
@@ -398,8 +384,6 @@ const columnsResultsModel = computed({
|
|
|
398
384
|
|
|
399
385
|
const isDragging = computed(() => draggingCount.value > 0)
|
|
400
386
|
|
|
401
|
-
provide('isDragging', isDragging)
|
|
402
|
-
|
|
403
387
|
const hasColumnsLength = computed(() => !!Object.keys(columnsResultsModel.value).length)
|
|
404
388
|
|
|
405
389
|
const containerStyle = computed(() => `width: ${props.columnWidth};`)
|
|
@@ -425,12 +409,54 @@ const defaultConfirmDialogProps = computed(() => {
|
|
|
425
409
|
}
|
|
426
410
|
})
|
|
427
411
|
|
|
412
|
+
// provide
|
|
413
|
+
provide('isDragging', isDragging)
|
|
414
|
+
|
|
415
|
+
// watchers
|
|
416
|
+
watch(
|
|
417
|
+
() => normalizedHeaders.value,
|
|
418
|
+
value => {
|
|
419
|
+
if (isLoadingUpdatePosition.value || !value?.length) return
|
|
420
|
+
|
|
421
|
+
fetchColumnsValues()
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
watch(() => columnContainerElements.value, () => {
|
|
426
|
+
setColumnHeightContainer()
|
|
427
|
+
handleElementsList()
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
// hooks
|
|
431
|
+
onMounted(() => {
|
|
432
|
+
if (normalizedHeaders.value.length) {
|
|
433
|
+
fetchColumnsValues()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
window.addEventListener('resize', setColumnHeightContainer)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
onBeforeUnmount(() => {
|
|
440
|
+
abortAllColumnRequests()
|
|
441
|
+
destroySortable()
|
|
442
|
+
|
|
443
|
+
window.removeEventListener('resize', setColumnHeightContainer)
|
|
444
|
+
})
|
|
445
|
+
|
|
428
446
|
// functions
|
|
429
447
|
/*
|
|
430
448
|
* Setar o tamanho do container do board, onde deverá ser a altura passada via prop, ou o default será ocupar o maximo
|
|
431
449
|
* de espaço que ele conseguir considerando a altura do container em relação ao topo.
|
|
432
450
|
*/
|
|
433
451
|
function setColumnHeightContainer () {
|
|
452
|
+
if (props.height) {
|
|
453
|
+
columnContainerElements.value.forEach(columnElement => {
|
|
454
|
+
columnElement.style.height = props.height
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
434
460
|
// Primeira etapa: calcula e aplica a altura inicial de cada coluna
|
|
435
461
|
columnContainerElements.value.forEach(columnElement => {
|
|
436
462
|
// Pega a posição atual da coluna em relação ao topo da viewport
|
|
@@ -490,7 +516,7 @@ async function fetchColumns () {
|
|
|
490
516
|
const mySession = ++currentFetchColumnsSession
|
|
491
517
|
|
|
492
518
|
// Mapeia os índices visíveis para os IDs de coluna correspondentes
|
|
493
|
-
const visibleKeys = new Set(visibleItems.value.map(
|
|
519
|
+
const visibleKeys = new Set(visibleItems.value.map(item => getKeyByHeader(normalizedHeaders.value[item])))
|
|
494
520
|
|
|
495
521
|
const visibleHeaders = visibleKeys.size
|
|
496
522
|
? normalizedHeaders.value.filter(header => visibleKeys.has(getKeyByHeader(header)))
|
|
@@ -524,6 +550,7 @@ async function fetchColumns () {
|
|
|
524
550
|
if (currentFetchColumnsSession !== mySession) return
|
|
525
551
|
|
|
526
552
|
const allPromises = [...visibleColumns, ...hiddenColumns]
|
|
553
|
+
console.log('🚀 ~ fetchColumns ~ allPromises:', allPromises)
|
|
527
554
|
|
|
528
555
|
const hasAllPromisesSucceeded = allPromises.every(promise => promise.status === 'fulfilled')
|
|
529
556
|
const hasAllPromisesFailed = allPromises.length > 0 && allPromises.every(promise => promise.status === 'rejected')
|
|
@@ -544,18 +571,36 @@ async function fetchColumns () {
|
|
|
544
571
|
}
|
|
545
572
|
|
|
546
573
|
/*
|
|
547
|
-
*
|
|
574
|
+
* Wrapper para chamadas de fetchColumn originadas de eventos de clique do usuário.
|
|
575
|
+
* fetchColumn relança o erro para que Promise.allSettled em fetchColumns consiga
|
|
576
|
+
* detectar colunas com falha; aqui o erro já foi tratado internamente, portanto
|
|
577
|
+
* é suprimido para evitar o warning "Unhandled error during execution of component
|
|
578
|
+
* event handler" do Vue.
|
|
548
579
|
*/
|
|
549
|
-
|
|
580
|
+
function handleFetchColumnClick (header) {
|
|
581
|
+
fetchColumn(header, true)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Busca a coluna com base no header recebido.
|
|
586
|
+
*
|
|
587
|
+
* @param header - payload do header da coluna a ser buscada, exemplo: { date: '2024-02-12', ... }
|
|
588
|
+
* @param shouldHideSkeleton - flag para controlar se o skeleton de carregamento deve ser ocultado durante a busca
|
|
589
|
+
*/
|
|
590
|
+
async function fetchColumn (header, shouldHideSkeleton) {
|
|
550
591
|
const headerKey = getKeyByHeader(header)
|
|
551
592
|
|
|
552
|
-
|
|
553
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Cancela qualquer request anterior para esta mesma coluna antes de iniciar a nova.
|
|
595
|
+
* Garante que nunca haja duas requests paralelas para a mesma coluna.
|
|
596
|
+
*/
|
|
554
597
|
abortColumnRequest(headerKey)
|
|
555
598
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Incrementa a geração desta coluna para invalidar callbacks onLoading da request cancelada,
|
|
601
|
+
* evitando que o onLoading(false) antigo sobrescreva o estado da nova request.
|
|
602
|
+
* Usa ?? 0 pois ++undefined retorna NaN, e NaN === NaN é sempre false, travando o skeleton.
|
|
603
|
+
*/
|
|
559
604
|
columnFetchGenerations[headerKey] = (columnFetchGenerations[headerKey] ?? 0) + 1
|
|
560
605
|
const generation = columnFetchGenerations[headerKey]
|
|
561
606
|
|
|
@@ -564,10 +609,10 @@ async function fetchColumn (header, fromSeeMore, setEr) {
|
|
|
564
609
|
|
|
565
610
|
const { limit, offset } = columnsPagination.value[headerKey] || {}
|
|
566
611
|
|
|
567
|
-
|
|
612
|
+
hideSkeleton.value = shouldHideSkeleton
|
|
568
613
|
|
|
569
614
|
const { data: response, error } = await promiseHandler(
|
|
570
|
-
axios.get(`${props.columnUrl}/${headerKey}
|
|
615
|
+
axios.get(`${props.columnUrl}/${headerKey}`, {
|
|
571
616
|
signal: abortController.signal,
|
|
572
617
|
params: {
|
|
573
618
|
...props.columnParams,
|
|
@@ -577,8 +622,10 @@ async function fetchColumn (header, fromSeeMore, setEr) {
|
|
|
577
622
|
}),
|
|
578
623
|
{
|
|
579
624
|
onLoading: value => {
|
|
580
|
-
|
|
581
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Só atualiza o loading se ainda for a request atual desta coluna,
|
|
627
|
+
* evitando que o onLoading(false) de uma request cancelada sobrescreva o da nova.
|
|
628
|
+
*/
|
|
582
629
|
if (columnFetchGenerations[headerKey] === generation) {
|
|
583
630
|
columnsLoading.value[headerKey] = value
|
|
584
631
|
}
|
|
@@ -589,7 +636,7 @@ async function fetchColumn (header, fromSeeMore, setEr) {
|
|
|
589
636
|
|
|
590
637
|
delete columnAbortControllers[headerKey]
|
|
591
638
|
|
|
592
|
-
|
|
639
|
+
hideSkeleton.value = false
|
|
593
640
|
|
|
594
641
|
if (error) {
|
|
595
642
|
// Request cancelada intencionalmente (novo fetchColumnsValues chamado): ignora silenciosamente.
|
|
@@ -623,7 +670,7 @@ async function fetchColumn (header, fromSeeMore, setEr) {
|
|
|
623
670
|
* onde cada item do objeto é uma coluna no board. O mesmo vale para "columnsFieldsModel", "columnsLoading" e
|
|
624
671
|
* "columnPagination", organizando os fields, loadings e o controle de paginação por chave identificadora do header.
|
|
625
672
|
*/
|
|
626
|
-
columnsResultsModel.value[headerKey] =
|
|
673
|
+
columnsResultsModel.value[headerKey] = hasDragAndDrop ? newColumnValues : markRaw(newColumnValues)
|
|
627
674
|
|
|
628
675
|
/*
|
|
629
676
|
* Pode acontecer das options nos fields da segunda página serem diferentes da primeira página,
|
|
@@ -731,15 +778,19 @@ function setColumnsPagination () {
|
|
|
731
778
|
|
|
732
779
|
columnsPagination.value[headerKey] = { limit: props.limitPerColumn, offset: 0 }
|
|
733
780
|
|
|
734
|
-
|
|
735
|
-
|
|
781
|
+
/**
|
|
782
|
+
* Inicia como true para exibir skeleton imediatamente, evitando flash de tela vazia
|
|
783
|
+
* entre o reset e o início das novas requests.
|
|
784
|
+
*/
|
|
736
785
|
columnsLoading.value[headerKey] = true
|
|
737
786
|
})
|
|
738
787
|
}
|
|
739
788
|
|
|
740
789
|
function fetchColumnsValues () {
|
|
741
|
-
|
|
742
|
-
|
|
790
|
+
/**
|
|
791
|
+
* Cancela todas as requests em andamento antes de iniciar novas, evitando resposta de requests
|
|
792
|
+
* antigas sobrescrevendo dados da nova sessão.
|
|
793
|
+
*/
|
|
743
794
|
abortAllColumnRequests()
|
|
744
795
|
|
|
745
796
|
lazyLoadingKey.value++
|
|
@@ -753,10 +804,10 @@ function fetchColumnsValues () {
|
|
|
753
804
|
/**
|
|
754
805
|
* Descrição:
|
|
755
806
|
* Exibe o texto quando:
|
|
756
|
-
* -
|
|
757
|
-
* -
|
|
758
|
-
* -
|
|
759
|
-
* -
|
|
807
|
+
* - Não está carregando a coluna
|
|
808
|
+
* - Não tem itens na coluna
|
|
809
|
+
* - Não estou fazendo o drag and drop
|
|
810
|
+
* - Não está exibindo o dialog de confirmação
|
|
760
811
|
*
|
|
761
812
|
* @param {Object} header
|
|
762
813
|
*/
|
|
@@ -811,19 +862,23 @@ function handleElementsList () {
|
|
|
811
862
|
* Seta a instancia do sortable, no qual varia de acordo com as props passadas.
|
|
812
863
|
*
|
|
813
864
|
* @param {HTMLElement} element
|
|
814
|
-
* @param {
|
|
865
|
+
* @param {number} index
|
|
815
866
|
*/
|
|
816
867
|
function setSortable (element, index) {
|
|
817
868
|
const defaultSortableConfig = {
|
|
818
|
-
animation:
|
|
869
|
+
animation: 150,
|
|
819
870
|
group: 'shared',
|
|
820
|
-
ghostClass: '
|
|
821
|
-
sort: false,
|
|
871
|
+
ghostClass: 'qas-board-generator__ghost',
|
|
822
872
|
swapThreshold: 1,
|
|
823
|
-
delay:
|
|
873
|
+
delay: 0,
|
|
824
874
|
delayOnTouchOnly: true,
|
|
825
875
|
emptyInsertThreshold: 0,
|
|
826
|
-
filter: '[data-disable-drag="true"]'
|
|
876
|
+
filter: '[data-disable-drag="true"]',
|
|
877
|
+
scrollSensitivity: HORIZONTAL_SCROLL_SENSITIVITY,
|
|
878
|
+
scrollSpeed: HORIZONTAL_SCROLL_SPEED,
|
|
879
|
+
bubbleScroll: true,
|
|
880
|
+
forceAutoScrollFallback: true,
|
|
881
|
+
scrollFn: handleSortableScroll
|
|
827
882
|
}
|
|
828
883
|
|
|
829
884
|
/**
|
|
@@ -839,15 +894,26 @@ function setSortable (element, index) {
|
|
|
839
894
|
let dropHandled = false
|
|
840
895
|
|
|
841
896
|
const sortable = new Sortable(element, {
|
|
842
|
-
sort: props.useDragAndDropY,
|
|
843
|
-
|
|
844
897
|
...defaultSortableConfig,
|
|
845
898
|
|
|
846
899
|
...props.sortableConfig,
|
|
847
900
|
|
|
901
|
+
/**
|
|
902
|
+
* `sort` deve vir APÓS os spreads para não ser sobrescrito pelo defaultSortableConfig.
|
|
903
|
+
* Quando useDragAndDropY é true, o sort precisa estar habilitado para que o SortableJS
|
|
904
|
+
* rastreie ativamente a posição do item dentro da coluna durante o drag. Sem isso,
|
|
905
|
+
* o item oscila entre colunas porque o Sortable do destino não "assume" o controle do item.
|
|
906
|
+
*/
|
|
907
|
+
sort: props.useDragAndDropY,
|
|
908
|
+
|
|
848
909
|
group: useOnlyDragAndDropY ? `column-${index}` : 'shared',
|
|
849
910
|
|
|
850
|
-
|
|
911
|
+
/**
|
|
912
|
+
* invertSwap muda o algoritmo de swap: em vez de trocar quando o centro do item arrastado
|
|
913
|
+
* cruza o centro do alvo (instável, causa oscilação), troca apenas quando cruza a BORDA
|
|
914
|
+
* superior/inferior do alvo. Isso cria uma "zona morta" que elimina o jitter.
|
|
915
|
+
*/
|
|
916
|
+
invertSwap: true,
|
|
851
917
|
|
|
852
918
|
onStart: () => {
|
|
853
919
|
dropHandled = false
|
|
@@ -871,6 +937,18 @@ function setSortable (element, index) {
|
|
|
871
937
|
...(props.useDragAndDropY && {
|
|
872
938
|
onSort: event => {
|
|
873
939
|
dropHandled = true
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* O SortableJS dispara onSort em AMBAS as listas envolvidas quando um item
|
|
943
|
+
* é arrastado entre colunas (cross-column): na lista de origem porque perdeu
|
|
944
|
+
* um item e na lista de destino porque ganhou um.
|
|
945
|
+
* O onAdd já trata o cross-column na lista de destino, então ao ignorar o
|
|
946
|
+
* onSort para eventos cross-column evitamos processar o mesmo drop duas vezes,
|
|
947
|
+
* o que causava corrupção do model (item duplicado no destino e remoção
|
|
948
|
+
* incorreta do último item da origem via splice(-1, 1)).
|
|
949
|
+
*/
|
|
950
|
+
if (event.from !== event.to) return
|
|
951
|
+
|
|
874
952
|
onDropCard(event)
|
|
875
953
|
}
|
|
876
954
|
})
|
|
@@ -885,6 +963,110 @@ function startDragging () {
|
|
|
885
963
|
|
|
886
964
|
function stopDragging () {
|
|
887
965
|
draggingCount.value = Math.max(0, draggingCount.value - 1)
|
|
966
|
+
resetSmoothScroll()
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Intercepta cada chamada de scroll do SortableJS.
|
|
971
|
+
* - Scroll vertical (colunas): retorna 'continue' para manter o comportamento nativo.
|
|
972
|
+
* - Scroll horizontal (container do board): aplica smooth scroll interpolado via rAF.
|
|
973
|
+
*
|
|
974
|
+
* rAF (requestAnimationFrame) sincroniza com o refresh da tela (~60fps),
|
|
975
|
+
* tornando o scroll suave. Sem rAF, seria um "salto" abrupto.
|
|
976
|
+
*/
|
|
977
|
+
function handleSortableScroll (offsetX, offsetY, evt, _touchEvt, scrollEl) {
|
|
978
|
+
// Obtém o container com scroll horizontal (`.qas-grabbable__container`).
|
|
979
|
+
const horizontalContainer = columnContainerElements.value[0]?.closest('.qas-grabbable__container')
|
|
980
|
+
|
|
981
|
+
// Acumula o offset no target e inicia a interpolação via rAF.
|
|
982
|
+
if (scrollEl === horizontalContainer && offsetX) {
|
|
983
|
+
if (smoothScrollEl !== scrollEl) {
|
|
984
|
+
smoothScrollTarget = scrollEl.scrollLeft
|
|
985
|
+
smoothScrollEl = scrollEl
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const maxScroll = scrollEl.scrollWidth - scrollEl.clientWidth
|
|
989
|
+
|
|
990
|
+
smoothScrollTarget = Math.max(0, Math.min(smoothScrollTarget + offsetX, maxScroll))
|
|
991
|
+
|
|
992
|
+
// Se não há animação em andamento, inicia uma.
|
|
993
|
+
// rAF (requestAnimationFrame) agenda a função smoothScrollStep para o próximo frame (~16ms a 60fps).
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Se não há animação em andamento, inicia uma. rAF (requestAnimationFrame) agenda a função smoothScrollStep
|
|
997
|
+
* para o próximo frame (~16ms a 60fps).
|
|
998
|
+
*/
|
|
999
|
+
if (!smoothScrollRafId) {
|
|
1000
|
+
smoothScrollRafId = requestAnimationFrame(smoothScrollStep)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* SortableJS chama essa função com offsets de scroll.
|
|
1008
|
+
* Para scroll vertical: retorna 'continue' para deixar o comportamento nativo.
|
|
1009
|
+
* Para scroll horizontal: aplica smooth scroll customizado via rAF.
|
|
1010
|
+
*/
|
|
1011
|
+
if (offsetY) {
|
|
1012
|
+
const clientY = evt?.clientY ?? evt?.touches?.[0]?.clientY
|
|
1013
|
+
|
|
1014
|
+
if (clientY !== undefined) {
|
|
1015
|
+
const rect = scrollEl.getBoundingClientRect()
|
|
1016
|
+
const distFromEdge = Math.min(clientY - rect.top, rect.bottom - clientY)
|
|
1017
|
+
|
|
1018
|
+
if (distFromEdge > VERTICAL_SCROLL_SENSITIVITY) return
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
scrollEl.scrollTop += offsetY * (VERTICAL_SCROLL_SPEED / HORIZONTAL_SCROLL_SPEED)
|
|
1022
|
+
return
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return 'continue'
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Loop de animação que interpola o scrollLeft em direção ao target
|
|
1030
|
+
* usando fator de lerp (move 20% da distância restante por frame).
|
|
1031
|
+
*
|
|
1032
|
+
* Funciona assim:
|
|
1033
|
+
* 1. Calcula a diferença entre a posição atual e a posição desejada (target).
|
|
1034
|
+
* 2. Se ainda há diferença significativa, move 20% da distância restante.
|
|
1035
|
+
* 3. Agenda o próximo frame via rAF para continuar a interpolação.
|
|
1036
|
+
* 4. Quando chega perto o suficiente (~0.5px), para a animação.
|
|
1037
|
+
*
|
|
1038
|
+
* Resultado: scroll suave e progressivo, não um "salto" abrupto.
|
|
1039
|
+
*/
|
|
1040
|
+
function smoothScrollStep () {
|
|
1041
|
+
smoothScrollRafId = null
|
|
1042
|
+
|
|
1043
|
+
if (!smoothScrollEl) return
|
|
1044
|
+
|
|
1045
|
+
const current = smoothScrollEl.scrollLeft
|
|
1046
|
+
const diff = smoothScrollTarget - current
|
|
1047
|
+
|
|
1048
|
+
// Se chegou bem perto do target, para na posição exata.
|
|
1049
|
+
if (Math.abs(diff) < 0.5) {
|
|
1050
|
+
smoothScrollEl.scrollLeft = smoothScrollTarget
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Move 20% da distância restante (interpolação suave).
|
|
1055
|
+
smoothScrollEl.scrollLeft = current + diff * SMOOTH_SCROLL_LERP
|
|
1056
|
+
|
|
1057
|
+
// Agenda o próximo frame para continuar a animação (chamada recursiva via rAF).
|
|
1058
|
+
smoothScrollRafId = requestAnimationFrame(smoothScrollStep)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function resetSmoothScroll () {
|
|
1062
|
+
// Cancela a animação agendada (rAF) para evitar que continue executando após o drag terminar.
|
|
1063
|
+
if (smoothScrollRafId) {
|
|
1064
|
+
cancelAnimationFrame(smoothScrollRafId)
|
|
1065
|
+
smoothScrollRafId = null
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
smoothScrollEl = null
|
|
1069
|
+
smoothScrollTarget = 0
|
|
888
1070
|
}
|
|
889
1071
|
|
|
890
1072
|
/**
|
|
@@ -919,7 +1101,7 @@ function addItemToList ({ headerKey, item, index }) {
|
|
|
919
1101
|
|
|
920
1102
|
updatedList.splice(insertIndex, 0, item)
|
|
921
1103
|
|
|
922
|
-
columnsResultsModel.value[headerKey] =
|
|
1104
|
+
columnsResultsModel.value[headerKey] = updatedList
|
|
923
1105
|
columnsPagination.value[headerKey].count += 1
|
|
924
1106
|
}
|
|
925
1107
|
|
|
@@ -1040,7 +1222,7 @@ function removeItemFromList ({ headerKey, itemId }) {
|
|
|
1040
1222
|
const updatedList = [...columnItemList]
|
|
1041
1223
|
updatedList.splice(itemIndex, 1)
|
|
1042
1224
|
|
|
1043
|
-
columnsResultsModel.value[headerKey] =
|
|
1225
|
+
columnsResultsModel.value[headerKey] = updatedList
|
|
1044
1226
|
|
|
1045
1227
|
/**
|
|
1046
1228
|
* Remove o item do count da coluna para não mostrar o botão de "Ver mais¨.
|
|
@@ -1048,13 +1230,29 @@ function removeItemFromList ({ headerKey, itemId }) {
|
|
|
1048
1230
|
columnsPagination.value[headerKey].count -= 1
|
|
1049
1231
|
}
|
|
1050
1232
|
|
|
1233
|
+
/**
|
|
1234
|
+
* Substitui um item na lista de uma coluna pelos dados atualizados do servidor.
|
|
1235
|
+
*
|
|
1236
|
+
* 1. Localiza a lista de itens da coluna
|
|
1237
|
+
* 2. Encontra o índice do item usando o itemIdKey como identificador
|
|
1238
|
+
* 3. Se encontrado, altera o item localmente (sem criar nova referência de array)
|
|
1239
|
+
*
|
|
1240
|
+
* @param {Object} params
|
|
1241
|
+
* @param {string} params.headerKey - ID da coluna
|
|
1242
|
+
* @param {string|number} params.itemId - ID do item a atualizar (deve corresponder a props.itemIdKey)
|
|
1243
|
+
* @param {Object} params.updatedItem - Novo objeto do item com dados atualizados
|
|
1244
|
+
*
|
|
1245
|
+
*/
|
|
1051
1246
|
function updateItemInList ({ headerKey, itemId, updatedItem }) {
|
|
1052
1247
|
const columnItemList = columnsResultsModel.value[headerKey]
|
|
1053
1248
|
|
|
1249
|
+
// Encontra o índice do item na lista usando o identificador (itemIdKey)
|
|
1054
1250
|
const itemIndex = columnItemList.findIndex(itemContent => itemContent[props.itemIdKey] === itemId)
|
|
1055
1251
|
|
|
1252
|
+
// Se não encontrar (itemIndex === -1), retorna sem fazer nada
|
|
1056
1253
|
if (!~itemIndex) return
|
|
1057
1254
|
|
|
1255
|
+
// Substitui o item antigo pelo item atualizado na mesma posição
|
|
1058
1256
|
columnItemList.splice(itemIndex, 1, updatedItem)
|
|
1059
1257
|
}
|
|
1060
1258
|
|
|
@@ -1077,7 +1275,7 @@ async function updatePosition ({ newHeaderKey, oldHeaderKey, itemId, event, opti
|
|
|
1077
1275
|
|
|
1078
1276
|
const isFnUpdatePositionUrl = typeof props.updatePositionUrl === 'function'
|
|
1079
1277
|
|
|
1080
|
-
|
|
1278
|
+
hideSkeleton.value = true
|
|
1081
1279
|
|
|
1082
1280
|
const url = isFnUpdatePositionUrl
|
|
1083
1281
|
? props.updatePositionUrl({ newHeaderKey, oldHeaderKey, itemId })
|
|
@@ -1182,7 +1380,7 @@ function isCancelledError (error) {
|
|
|
1182
1380
|
* @param {string|number} params.itemId - ID do item a ser movido (valor correspondente à prop `itemIdKey`).
|
|
1183
1381
|
* @param {string|number} params.fromColumnId - ID da coluna de origem (valor correspondente à prop `columnIdKey`).
|
|
1184
1382
|
* @param {string|number} params.toColumnId - ID da coluna de destino (valor correspondente à prop `columnIdKey`).
|
|
1185
|
-
* @param {Object}
|
|
1383
|
+
* @param {Object} [params.updatedItem] - Dados atualizados do item. Quando informado, sobrescreve o item original na coluna de destino.
|
|
1186
1384
|
* @returns {void}
|
|
1187
1385
|
*/
|
|
1188
1386
|
function transferItemToColumn ({ itemId, fromColumnId, toColumnId, updatedItem }) {
|
|
@@ -1202,10 +1400,10 @@ function transferItemToColumn ({ itemId, fromColumnId, toColumnId, updatedItem }
|
|
|
1202
1400
|
function hasSkeletonByHeader (header) {
|
|
1203
1401
|
const headerKey = getKeyByHeader(header)
|
|
1204
1402
|
|
|
1205
|
-
return (props.skeleton || columnsLoading.value[headerKey]) && !
|
|
1403
|
+
return (props.skeleton || columnsLoading.value[headerKey]) && !hideSkeleton.value
|
|
1206
1404
|
}
|
|
1207
1405
|
|
|
1208
|
-
function
|
|
1406
|
+
function getColumnClasses (header) {
|
|
1209
1407
|
const headerKey = getKeyByHeader(header)
|
|
1210
1408
|
|
|
1211
1409
|
return {
|
|
@@ -1252,5 +1450,28 @@ function getCardsContainerProps (header) {
|
|
|
1252
1450
|
&__column-error {
|
|
1253
1451
|
border: 1px solid $negative;
|
|
1254
1452
|
}
|
|
1453
|
+
|
|
1454
|
+
&__ghost {
|
|
1455
|
+
border: 1px solid $grey-4 !important;
|
|
1456
|
+
overflow: hidden;
|
|
1457
|
+
border-radius: $generic-border-radius;
|
|
1458
|
+
position: relative;
|
|
1459
|
+
margin-bottom: var(--qas-spacing-sm);
|
|
1460
|
+
|
|
1461
|
+
&::after {
|
|
1462
|
+
align-items: center;
|
|
1463
|
+
background-color: $grey-3;
|
|
1464
|
+
border-radius: inherit;
|
|
1465
|
+
color: $grey-8;
|
|
1466
|
+
content: 'place_item';
|
|
1467
|
+
display: flex;
|
|
1468
|
+
font-family: 'Material Symbols Rounded';
|
|
1469
|
+
font-size: 40px;
|
|
1470
|
+
inset: 0;
|
|
1471
|
+
justify-content: center;
|
|
1472
|
+
position: absolute;
|
|
1473
|
+
z-index: 999;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1255
1476
|
}
|
|
1256
1477
|
</style>
|
|
@@ -97,8 +97,8 @@ props:
|
|
|
97
97
|
type: Number
|
|
98
98
|
default: 12
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
desc:
|
|
100
|
+
skeleton:
|
|
101
|
+
desc: Exibe o estado de esqueleto de carregamento das colunas. Enquanto `true`, cada coluna renderiza cards fictícios no lugar dos itens reais.
|
|
102
102
|
type: Boolean
|
|
103
103
|
default: true
|
|
104
104
|
|
|
@@ -150,11 +150,26 @@ slots:
|
|
|
150
150
|
column-item:
|
|
151
151
|
desc: Slot para acessar o item da coluna.
|
|
152
152
|
scope:
|
|
153
|
+
column-index:
|
|
154
|
+
desc: Índice da coluna atual.
|
|
155
|
+
type: Number
|
|
156
|
+
default: 0
|
|
157
|
+
|
|
153
158
|
fields:
|
|
154
159
|
desc: Fields referente à coluna atual do template.
|
|
155
160
|
type: Object
|
|
156
161
|
default: {}
|
|
157
162
|
|
|
163
|
+
header:
|
|
164
|
+
desc: Informações do header da coluna atual.
|
|
165
|
+
type: Object
|
|
166
|
+
default: {}
|
|
167
|
+
|
|
168
|
+
is-updating-position:
|
|
169
|
+
desc: Indica se o item está sendo atualizado via drag-and-drop (aguardando retorno da API de update).
|
|
170
|
+
type: Boolean
|
|
171
|
+
default: false
|
|
172
|
+
|
|
158
173
|
item:
|
|
159
174
|
desc: Informações de cada item da coluna que foi buscado através da API.
|
|
160
175
|
type: Object
|
|
@@ -220,6 +235,45 @@ methods:
|
|
|
220
235
|
'fetchColumn: (header) => void':
|
|
221
236
|
desc: Busca uma coluna específica com base no header fornecido.
|
|
222
237
|
|
|
238
|
+
'refreshColumn: (header) => void':
|
|
239
|
+
desc: Reinicia a paginação da coluna e refaz a busca, substituindo completamente os itens existentes.
|
|
240
|
+
params:
|
|
241
|
+
header:
|
|
242
|
+
desc: Header da coluna a ser recarregada (mesmo formato da prop `headers`).
|
|
243
|
+
type: Object
|
|
244
|
+
required: true
|
|
245
|
+
|
|
246
|
+
'removeItemFromList: ({ headerKey, itemId }) => void':
|
|
247
|
+
desc: Remove um item da lista de uma coluna localmente, sem realizar nenhuma requisição. Também decrementa o contador de paginação.
|
|
248
|
+
params:
|
|
249
|
+
headerKey:
|
|
250
|
+
desc: Chave identificadora da coluna de origem (valor correspondente à prop `column-id-key`).
|
|
251
|
+
type: String | Number
|
|
252
|
+
required: true
|
|
253
|
+
itemId:
|
|
254
|
+
desc: ID do item a ser removido (valor correspondente à prop `item-id-key`).
|
|
255
|
+
type: String | Number
|
|
256
|
+
required: true
|
|
257
|
+
|
|
258
|
+
'updateItemInList: ({ headerKey, itemId, updatedItem }) => void':
|
|
259
|
+
desc: Substitui os dados de um item existente em uma coluna localmente, sem realizar nenhuma requisição.
|
|
260
|
+
params:
|
|
261
|
+
headerKey:
|
|
262
|
+
desc: Chave identificadora da coluna (valor correspondente à prop `column-id-key`).
|
|
263
|
+
type: String | Number
|
|
264
|
+
required: true
|
|
265
|
+
itemId:
|
|
266
|
+
desc: ID do item a ser atualizado (valor correspondente à prop `item-id-key`).
|
|
267
|
+
type: String | Number
|
|
268
|
+
required: true
|
|
269
|
+
updatedItem:
|
|
270
|
+
desc: Dados atualizados que substituirão o item original.
|
|
271
|
+
type: Object
|
|
272
|
+
required: true
|
|
273
|
+
|
|
274
|
+
'refetchColumns: () => void':
|
|
275
|
+
desc: Alias de `fetchColumns`. Busca novamente todas as colunas com base nos headers fornecidos.
|
|
276
|
+
|
|
223
277
|
'transferItemToColumn: ({ itemId, fromColumnId, toColumnId, updatedItem? }) => void':
|
|
224
278
|
desc: |
|
|
225
279
|
Transfere um item de uma coluna para outra localmente, sem realizar nenhuma requisição.
|
|
@@ -165,6 +165,12 @@ const hasDatePicker = computed(() => !props.useTimeOnly && !props.readonly)
|
|
|
165
165
|
const hasTimePicker = computed(() => !props.useDateOnly && !props.readonly)
|
|
166
166
|
|
|
167
167
|
watch(() => props.modelValue, (current, original) => {
|
|
168
|
+
/**
|
|
169
|
+
* No caso de ser adicionado uma data incompleta ou inválida, vamos limpar o model,
|
|
170
|
+
* mas não vamos limpar o "currentValue", para não limpar o campo, somente o model.
|
|
171
|
+
*/
|
|
172
|
+
if (!current && original && error.value) return
|
|
173
|
+
|
|
168
174
|
if (!current || props.useTimeOnly) {
|
|
169
175
|
currentValue.value = current
|
|
170
176
|
return
|
|
@@ -217,6 +223,9 @@ function updateModelValue (value) {
|
|
|
217
223
|
if (error.value) {
|
|
218
224
|
hasInvalidDate.value = true
|
|
219
225
|
errorMessage.value = 'Data inválida.'
|
|
226
|
+
|
|
227
|
+
emit('update:modelValue', '')
|
|
228
|
+
|
|
220
229
|
return
|
|
221
230
|
}
|
|
222
231
|
|
|
@@ -262,18 +271,33 @@ function validateDateAndTime (value) {
|
|
|
262
271
|
function validateDateTimeOnBlur () {
|
|
263
272
|
const valueLength = currentValue.value?.replace?.(/_/g, '')?.length
|
|
264
273
|
|
|
274
|
+
// Caso for datetime
|
|
275
|
+
if (!props.useDateOnly && !props.useTimeOnly) {
|
|
276
|
+
const [date, time] = (currentValue.value || '').split(' ') || []
|
|
277
|
+
const isValidDate = date?.replace?.(/_/g, '')?.length === props.dateMask.length
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Caso tenha preenchido a data, a data seja válida e não tenha preenchido o horário,
|
|
281
|
+
* preenche o horário com 00:00, ou de acordo com a mask.
|
|
282
|
+
*/
|
|
283
|
+
if (isValidDate && !validateDateOnly(date) && !time?.length) {
|
|
284
|
+
// Horario setado deve seguir a mascara.
|
|
285
|
+
const defaultTimeByMask = props.timeMask.replace(/[HhMmSs]/g, '0')
|
|
286
|
+
const newValue = `${date} ${defaultTimeByMask}`
|
|
287
|
+
|
|
288
|
+
currentValue.value = newValue
|
|
289
|
+
updateModelValue(newValue)
|
|
290
|
+
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
265
295
|
// valida se o tamanho digitado é o tamanho que a mascara espera receber
|
|
266
296
|
error.value = !!((valueLength < mask.value.length || error.value) && valueLength)
|
|
267
297
|
|
|
268
298
|
if (error.value && !hasInvalidDate.value) {
|
|
269
299
|
errorMessage.value = 'Data incompleta.'
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (hasInvalidDate.value) {
|
|
273
|
-
currentValue.value = ''
|
|
274
|
-
}
|
|
275
300
|
|
|
276
|
-
if (error.value || hasInvalidDate.value) {
|
|
277
301
|
emit('update:modelValue', '')
|
|
278
302
|
}
|
|
279
303
|
}
|
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
<slot name="header">
|
|
6
6
|
<div class="items-center justify-between row">
|
|
7
7
|
<qas-label data-cy="dialog-title" :label="props.card.title" margin="none">
|
|
8
|
-
<slot name="title"
|
|
8
|
+
<slot name="title" />
|
|
9
9
|
</qas-label>
|
|
10
|
-
<!-- <slot name="title">
|
|
11
|
-
</slot> -->
|
|
12
10
|
|
|
13
11
|
<qas-btn v-if="isInfoDialog" v-close-popup color="grey-10" data-cy="dialog-close-btn" icon="sym_r_close" variant="tertiary" />
|
|
14
12
|
</div>
|
|
@@ -69,6 +69,11 @@ slots:
|
|
|
69
69
|
header:
|
|
70
70
|
desc: Slot para o título.
|
|
71
71
|
|
|
72
|
+
provide:
|
|
73
|
+
is-dialog:
|
|
74
|
+
desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasDialog.
|
|
75
|
+
type: Boolean
|
|
76
|
+
|
|
72
77
|
events:
|
|
73
78
|
'@update:model-value -> function (value)':
|
|
74
79
|
desc: Dispara toda vez que o model é atualizado, também utilizado para v-model.
|
|
@@ -111,6 +116,9 @@ selectors:
|
|
|
111
116
|
desc: Seletor do botão de confirmar do componente.
|
|
112
117
|
examples: ['data-cy="dialog-ok-btn"']
|
|
113
118
|
|
|
119
|
+
title:
|
|
120
|
+
desc: Slot para inserir conteúdo adicional ao lado do título do card do dialog.
|
|
121
|
+
|
|
114
122
|
dialog-title:
|
|
115
123
|
desc: Seletor do título do componente.
|
|
116
124
|
examples: ['data-cy="dialog-title"']
|
|
@@ -62,6 +62,11 @@ slots:
|
|
|
62
62
|
content:
|
|
63
63
|
desc: Slot para substituir o conteúdo principal do card.
|
|
64
64
|
|
|
65
|
+
provide:
|
|
66
|
+
is-expansion-item:
|
|
67
|
+
desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasExpansionItem.
|
|
68
|
+
type: Boolean
|
|
69
|
+
|
|
65
70
|
events:
|
|
66
71
|
'@update:model-value -> function(value)':
|
|
67
72
|
desc: Dispara quando o model-value altera, também usado para v-model.
|
|
@@ -120,6 +120,11 @@ slots:
|
|
|
120
120
|
'legend-bottom[nome-da-chave-do-fieldset]-[nome-da-chave-do-subset]':
|
|
121
121
|
desc: Acessa o slot da seção abaixo do conteúdo do form de um subset específico.
|
|
122
122
|
|
|
123
|
+
provide:
|
|
124
|
+
is-form-generator:
|
|
125
|
+
desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasFormGenerator.
|
|
126
|
+
type: Boolean
|
|
127
|
+
|
|
123
128
|
events:
|
|
124
129
|
'@update:model-value -> function(value)':
|
|
125
130
|
desc: Dispara quando o model-value altera, também usado para v-model.
|
|
@@ -54,6 +54,11 @@ props:
|
|
|
54
54
|
type: Boolean
|
|
55
55
|
default: false
|
|
56
56
|
|
|
57
|
+
provide:
|
|
58
|
+
is-header:
|
|
59
|
+
desc: Identificador booleano que indica quando um componente está dentro do contexto de um QasHeader.
|
|
60
|
+
type: Boolean
|
|
61
|
+
|
|
57
62
|
slots:
|
|
58
63
|
actions:
|
|
59
64
|
desc: slot para acessar seção da direita (ações).
|
|
@@ -30,35 +30,28 @@ import { ref, onMounted, onBeforeUnmount, useSlots, nextTick, watch } from 'vue'
|
|
|
30
30
|
|
|
31
31
|
defineOptions({ name: 'QasLazyLoadingComponents' })
|
|
32
32
|
|
|
33
|
-
// const emit = defineEmits(['update:visibleItems'])
|
|
34
|
-
|
|
35
33
|
const props = defineProps({
|
|
36
|
-
// Porcentagem de visibilidade necessária para ativar (0.0 a 1.0)
|
|
37
34
|
threshold: {
|
|
38
35
|
type: Number,
|
|
39
36
|
default: 0.1 // 10% visível
|
|
40
37
|
},
|
|
41
38
|
|
|
42
|
-
// Margem extra ao redor do viewport para pré-carregamento
|
|
43
39
|
rootMargin: {
|
|
44
40
|
type: String,
|
|
45
41
|
default: '0px'
|
|
46
42
|
},
|
|
47
43
|
|
|
48
|
-
// Direção do scroll: 'vertical' (padrão) ou 'horizontal'
|
|
49
44
|
direction: {
|
|
50
45
|
type: String,
|
|
51
46
|
default: 'vertical',
|
|
52
47
|
validator: value => ['vertical', 'horizontal'].includes(value)
|
|
53
48
|
},
|
|
54
49
|
|
|
55
|
-
// Altura dos placeholders antes de carregar (usado em direction='vertical')
|
|
56
50
|
placeholderHeight: {
|
|
57
51
|
type: String,
|
|
58
52
|
default: '500px'
|
|
59
53
|
},
|
|
60
54
|
|
|
61
|
-
// Largura dos placeholders antes de carregar (usado em direction='horizontal')
|
|
62
55
|
placeholderWidth: {
|
|
63
56
|
type: String,
|
|
64
57
|
default: '300px'
|
|
@@ -4,21 +4,46 @@ meta:
|
|
|
4
4
|
desc: Componente para carregar elementos do slot somente quando ficam visíveis na viewport, otimizando performance da página.
|
|
5
5
|
|
|
6
6
|
props:
|
|
7
|
-
|
|
8
|
-
desc:
|
|
9
|
-
default: 0.1
|
|
10
|
-
type: Number
|
|
11
|
-
|
|
12
|
-
root-margin:
|
|
13
|
-
desc: Margem extra ao redor do viewport para pré-carregamento
|
|
14
|
-
default: '0px'
|
|
7
|
+
direction:
|
|
8
|
+
desc: Direção do scroll e do lazy loading. Use `vertical` (padrão) para listas de rolagem vertical e `horizontal` para listas de rolagem horizontal.
|
|
15
9
|
type: String
|
|
10
|
+
default: vertical
|
|
11
|
+
values: vertical | horizontal
|
|
16
12
|
|
|
17
13
|
placeholder-height:
|
|
18
|
-
desc: Altura do placeholder exibido enquanto o elemento não está visível
|
|
14
|
+
desc: Altura do placeholder exibido enquanto o elemento não está visível (usado quando `direction` é `vertical`).
|
|
19
15
|
default: '500px'
|
|
20
16
|
type: String
|
|
21
17
|
|
|
18
|
+
placeholder-width:
|
|
19
|
+
desc: Largura do placeholder exibido enquanto o elemento não está visível (usado quando `direction` é `horizontal`).
|
|
20
|
+
default: '300px'
|
|
21
|
+
type: String
|
|
22
|
+
|
|
23
|
+
root-margin:
|
|
24
|
+
desc: Margem extra ao redor do viewport para pré-carregamento
|
|
25
|
+
default: '0px'
|
|
26
|
+
type: String
|
|
27
|
+
|
|
28
|
+
threshold:
|
|
29
|
+
desc: Threshold do IntersectionObserver (0 = aparece um pouco, 1 = totalmente visível)
|
|
30
|
+
default: 0.1
|
|
31
|
+
type: Number
|
|
32
|
+
|
|
33
|
+
visible-items:
|
|
34
|
+
desc: Expõe os índices dos itens atualmente visíveis na viewport. Útil para otimizar renders e priorizar carregamento de dados.
|
|
35
|
+
type: Array
|
|
36
|
+
default: []
|
|
37
|
+
model: true
|
|
38
|
+
|
|
22
39
|
slots:
|
|
23
40
|
default:
|
|
24
41
|
desc: Elementos/componentes que serão carregados conforme ficam visíveis, onde cada elemento irmão dentro do slot será tratado individualmente para o lazy loading.
|
|
42
|
+
|
|
43
|
+
events:
|
|
44
|
+
'@update:visible-items -> function(value)':
|
|
45
|
+
desc: Dispara toda vez que os índices dos itens visíveis são atualizados, também utilizado para v-model:visible-items.
|
|
46
|
+
params:
|
|
47
|
+
value:
|
|
48
|
+
desc: Array de índices dos itens atualmente visíveis na viewport.
|
|
49
|
+
type: Array
|
|
@@ -229,7 +229,7 @@ function getDialogSlot (name) {
|
|
|
229
229
|
|
|
230
230
|
// ------------------------- composable functions ------------------------------
|
|
231
231
|
function useList () {
|
|
232
|
-
const filteredOptions = ref(props.options)
|
|
232
|
+
const filteredOptions = ref([...props.options])
|
|
233
233
|
|
|
234
234
|
const selectedOptions = computed(() => {
|
|
235
235
|
const options = []
|
|
@@ -206,6 +206,27 @@ function previous () {
|
|
|
206
206
|
border-radius: var(--qas-generic-border-radius);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
.q-stepper__tab {
|
|
210
|
+
/*
|
|
211
|
+
* Por padrão o quasar sempre deixa as demais linhas com o mesmo tamanho, exceto a primeira e a última, que são
|
|
212
|
+
* maiores devido terem alinhamento inicial e final respectivamente, enquanto as demais possuem alinhamento
|
|
213
|
+
* central. Para equalizar visualmente os tamanhos, é necessário setar uma proporção de flex diferente para a
|
|
214
|
+
* primeira/última tab e para as tabs do meio.
|
|
215
|
+
*
|
|
216
|
+
* Proporção 2:3 para equalizar visualmente os tamanhos:
|
|
217
|
+
* - Primeira/última tab: flex 2 (possuem apenas meia linha)
|
|
218
|
+
* - Tabs do meio: flex 3 (possuem linha completa dos dois lados)
|
|
219
|
+
*/
|
|
220
|
+
&:first-child,
|
|
221
|
+
&:last-child {
|
|
222
|
+
flex: 2;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
&:not(:first-child):not(:last-child) {
|
|
226
|
+
flex: 3;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
209
230
|
.q-stepper {
|
|
210
231
|
&__tab {
|
|
211
232
|
&--done {
|
|
@@ -223,9 +244,10 @@ function previous () {
|
|
|
223
244
|
}
|
|
224
245
|
}
|
|
225
246
|
|
|
226
|
-
// Seta a cor do after da linha quando a step está finalizada, é a primeira step ou a próxima é ativa.
|
|
247
|
+
// Seta a cor do after da linha quando a step está finalizada, é a primeira step ou a próxima é ativa ou finalizada.
|
|
227
248
|
&:first-child,
|
|
228
|
-
&:has(+ .q-stepper__tab.q-stepper__tab--active)
|
|
249
|
+
&:has(+ .q-stepper__tab.q-stepper__tab--active),
|
|
250
|
+
&:has(+ .q-stepper__tab.q-stepper__tab--done) {
|
|
229
251
|
.q-stepper__line::after {
|
|
230
252
|
background-color: var(--q-primary);
|
|
231
253
|
}
|
|
@@ -7,8 +7,12 @@ import { computed, reactive } from 'vue'
|
|
|
7
7
|
* isSmall: boolean,
|
|
8
8
|
* isMedium: boolean,
|
|
9
9
|
* isLarge: boolean,
|
|
10
|
+
* isXLarge: boolean,
|
|
11
|
+
* is2XLarge: boolean,
|
|
10
12
|
* untilMedium: boolean,
|
|
11
13
|
* untilLarge: boolean,
|
|
14
|
+
* untilXLarge: boolean,
|
|
15
|
+
* until2XLarge: boolean,
|
|
12
16
|
* isMobile: boolean
|
|
13
17
|
* }}
|
|
14
18
|
*
|
|
@@ -25,15 +29,27 @@ export default function () {
|
|
|
25
29
|
// de 600 até 1023px
|
|
26
30
|
isMedium: computed(() => Screen.sm),
|
|
27
31
|
|
|
28
|
-
//
|
|
32
|
+
// Maior que 1023px
|
|
29
33
|
isLarge: computed(() => Screen.gt.sm),
|
|
30
34
|
|
|
35
|
+
// Maior que 1439px
|
|
36
|
+
isXLarge: computed(() => Screen.gt.md),
|
|
37
|
+
|
|
38
|
+
// Maior que 1919px
|
|
39
|
+
is2XLarge: computed(() => Screen.gt.lg),
|
|
40
|
+
|
|
31
41
|
// de 0 até 599px
|
|
32
42
|
untilMedium: computed(() => Screen.lt.sm),
|
|
33
43
|
|
|
34
44
|
// de 0 ate 1023px
|
|
35
45
|
untilLarge: computed(() => Screen.lt.md),
|
|
36
46
|
|
|
47
|
+
// de 0 até 1439px
|
|
48
|
+
untilXLarge: computed(() => Screen.lt.lg),
|
|
49
|
+
|
|
50
|
+
// de 0 até 1919px
|
|
51
|
+
until2XLarge: computed(() => Screen.lt.xl),
|
|
52
|
+
|
|
37
53
|
// Plataforma
|
|
38
54
|
isMobile: computed(() => Platform.is.mobile || false)
|
|
39
55
|
})
|
package/src/helpers/filters.js
CHANGED
|
@@ -160,7 +160,7 @@ function parseValue (value) {
|
|
|
160
160
|
try { return JSON.parse(value) } catch { return value }
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function booleanLabel (value, trueLabel = '
|
|
163
|
+
function booleanLabel (value, trueLabel = 'Sim', falseLabel = 'Não') {
|
|
164
164
|
try { return JSON.parse(value) ? trueLabel : falseLabel } catch { return value }
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -174,13 +174,16 @@ export default function setScrollGradient (config = {}) {
|
|
|
174
174
|
const elementRect = element.getBoundingClientRect()
|
|
175
175
|
const parentRect = element.parentElement.getBoundingClientRect()
|
|
176
176
|
|
|
177
|
+
// pequena margem para garantir que o gradiente fique grudado ao elemento.
|
|
178
|
+
const safeArea = 1
|
|
179
|
+
|
|
177
180
|
/**
|
|
178
181
|
* diferença entre o bottom do pai e o bottom do filho, valor positivo significa
|
|
179
182
|
* que há espaço livre abaixo do filho.
|
|
180
183
|
*/
|
|
181
184
|
const distance = {
|
|
182
|
-
end: isVertical ? (parentRect.bottom - elementRect.bottom) : (parentRect.right - elementRect.right),
|
|
183
|
-
start: isVertical ? (elementRect.top - parentRect.top) : (elementRect.left - parentRect.left)
|
|
185
|
+
end: isVertical ? (parentRect.bottom - elementRect.bottom - safeArea) : (parentRect.right - elementRect.right - safeArea),
|
|
186
|
+
start: isVertical ? (elementRect.top - parentRect.top) - safeArea : (elementRect.left - parentRect.left) - safeArea
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
span.style[getDirection(direction)] = `${distance[direction]}px`
|
|
@@ -3,7 +3,7 @@ type: component
|
|
|
3
3
|
meta:
|
|
4
4
|
desc: Plugin que implementa a action `destroy` do `StoreModule` adicionando comportamento de confirmação antes de excluir, este mesmo plugin é utilizado no componente `QasDelete`.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
provide:
|
|
7
7
|
'this.$qas.delete(config)':
|
|
8
8
|
params:
|
|
9
9
|
config:
|
|
@@ -4,15 +4,31 @@ export default () => {
|
|
|
4
4
|
const screensModel = {
|
|
5
5
|
// até 599px
|
|
6
6
|
isSmall: () => Screen.xs,
|
|
7
|
+
|
|
7
8
|
// de 600 até 1023px
|
|
8
9
|
isMedium: () => Screen.sm,
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
// Maior que 1023px
|
|
10
12
|
isLarge: () => Screen.gt.sm,
|
|
13
|
+
|
|
14
|
+
// Maior que 1439px
|
|
15
|
+
isXLarge: () => Screen.gt.md,
|
|
16
|
+
|
|
17
|
+
// Maior que 1919px
|
|
18
|
+
is2XLarge: () => Screen.gt.lg,
|
|
19
|
+
|
|
11
20
|
// de 0 até 599px
|
|
12
21
|
untilMedium: () => Screen.lt.sm,
|
|
22
|
+
|
|
13
23
|
// de 0 ate 1023px
|
|
14
24
|
untilLarge: () => Screen.lt.md,
|
|
15
25
|
|
|
26
|
+
// de 0 até 1439px
|
|
27
|
+
untilXLarge: () => Screen.lt.lg,
|
|
28
|
+
|
|
29
|
+
// de 0 até 1919px
|
|
30
|
+
until2XLarge: () => Screen.lt.xl,
|
|
31
|
+
|
|
16
32
|
// Plataforma
|
|
17
33
|
isMobile: () => Platform.is.mobile || false
|
|
18
34
|
}
|
|
@@ -3,7 +3,7 @@ type: component
|
|
|
3
3
|
meta:
|
|
4
4
|
desc: Plugin que implementa o "Screen" do quasar.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
provide:
|
|
7
7
|
'this.$qas.screen':
|
|
8
8
|
type: Object
|
|
9
9
|
debugger: true
|
|
@@ -11,6 +11,10 @@ inject:
|
|
|
11
11
|
isSmall: false
|
|
12
12
|
isMedium: false
|
|
13
13
|
isLarge: false
|
|
14
|
+
isXLarge: false
|
|
15
|
+
is2XLarge: false
|
|
14
16
|
untilMedium: false
|
|
15
17
|
untilLarge: false
|
|
18
|
+
untilXLarge: false
|
|
19
|
+
until2XLarge: false
|
|
16
20
|
isMobile: false
|