@bildvitta/quasar-ui-asteroid 3.17.0-beta.8 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +3 -2
  2. package/src/assets/sounds/nave-notification.mp3 +0 -0
  3. package/src/components/app-menu/QasAppMenu.vue +73 -15
  4. package/src/components/app-menu/QasAppMenu.yml +5 -5
  5. package/src/components/app-user/QasAppUser.vue +49 -40
  6. package/src/components/avatar/QasAvatar.vue +7 -8
  7. package/src/components/badge/QasBadge.vue +3 -29
  8. package/src/components/board-generator/QasBoardGenerator.vue +442 -40
  9. package/src/components/board-generator/QasBoardGenerator.yml +107 -12
  10. package/src/components/card/QasCard.vue +13 -4
  11. package/src/components/chart-view/QasChartView.vue +56 -3
  12. package/src/components/chart-view/QasChartView.yml +6 -0
  13. package/src/components/checkbox/QasCheckbox.vue +67 -11
  14. package/src/components/checkbox/QasCheckbox.yml +18 -0
  15. package/src/components/copy/QasCopy.vue +12 -2
  16. package/src/components/copy/QasCopy.yml +8 -0
  17. package/src/components/expansion-item/QasExpansionItem.vue +108 -76
  18. package/src/components/expansion-item/QasExpansionItem.yml +38 -10
  19. package/src/components/field/QasField.vue +1 -1
  20. package/src/components/form-generator/QasFormGenerator.vue +23 -10
  21. package/src/components/form-generator/QasFormGenerator.yml +2 -2
  22. package/src/components/grabbable/QasGrabbable.vue +14 -6
  23. package/src/components/grabbable/QasGrabbable.yml +4 -0
  24. package/src/components/grid-generator/QasGridGenerator.vue +3 -3
  25. package/src/components/grid-generator/QasGridGenerator.yml +2 -2
  26. package/src/components/grid-item/QasGridItem.vue +1 -1
  27. package/src/components/header/QasHeader.vue +11 -9
  28. package/src/components/infinite-scroll/QasInfiniteScroll.vue +16 -17
  29. package/src/components/infinite-scroll/QasInfiniteScroll.yml +7 -0
  30. package/src/components/list-view/QasListView.vue +16 -2
  31. package/src/components/list-view/QasListView.yml +9 -0
  32. package/src/components/radio/QasRadio.vue +24 -5
  33. package/src/components/radio/QasRadio.yml +6 -0
  34. package/src/components/select/QasSelect.vue +54 -7
  35. package/src/components/select/QasSelect.yml +6 -1
  36. package/src/components/select-filter/QasSelectFilter.vue +65 -0
  37. package/src/components/select-filter/QasSelectFilter.yml +36 -0
  38. package/src/components/stepper/QasStepper.vue +50 -3
  39. package/src/components/stepper-form-view/QasStepperFormView.vue +6 -4
  40. package/src/components/stepper-form-view/QasStepperFormView.yml +1 -1
  41. package/src/components/table-generator/QasTableGenerator.vue +3 -0
  42. package/src/components/text-truncate/QasTextTruncate.vue +77 -14
  43. package/src/components/text-truncate/QasTextTruncate.yml +14 -3
  44. package/src/components/uploader/QasUploader.vue +70 -24
  45. package/src/components/uploader/QasUploader.yml +15 -1
  46. package/src/components/welcome/QasWelcome.vue +8 -0
  47. package/src/components/welcome/QasWelcome.yml +3 -0
  48. package/src/composables/index.js +3 -1
  49. package/src/composables/private/index.js +1 -0
  50. package/src/composables/private/use-auth-user.js +20 -0
  51. package/src/composables/private/use-generator.js +20 -5
  52. package/src/composables/use-default-filters.js +106 -0
  53. package/src/composables/use-notifications.js +14 -0
  54. package/src/composables/use-query-cache.js +1 -1
  55. package/src/css/components/field.scss +13 -6
  56. package/src/helpers/set-scroll-on-grab.js +9 -1
  57. package/src/shared/badge-config.js +29 -0
  58. package/src/vue-plugin.js +3 -0
  59. package/src/components/app-menu/private/PvAppMenuHelpChat.vue +0 -222
@@ -2,7 +2,17 @@
2
2
  <div ref="parent" :class="classes">
3
3
  <div class="no-wrap row text-no-wrap">
4
4
  <div ref="truncate" class="ellipsis">
5
- <slot>{{ formattedText }}</slot>
5
+ <slot>
6
+ <div v-if="hasBadges" class="items-center q-col-gutter-sm row" :class="badgeParentClasses">
7
+ <div v-for="(item, index) in normalizedBadgesList" :key="index">
8
+ <qas-badge v-bind="getBadgeProps(item)" />
9
+ </div>
10
+ </div>
11
+
12
+ <div v-else class="ellipsis">
13
+ {{ formattedText }}
14
+ </div>
15
+ </slot>
6
16
  </div>
7
17
 
8
18
  <qas-btn v-if="hasButton" class="q-ml-sm" :label="buttonLabel" @click.stop.prevent="toggle" />
@@ -10,7 +20,7 @@
10
20
 
11
21
  <qas-dialog v-model="show" v-bind="defaultProps" aria-label="Diálogo de texto completo" role="dialog">
12
22
  <template v-if="isCounterMode" #description>
13
- <div class="q-col-gutter-y-md row">
23
+ <div class="q-col-gutter-y-sm row">
14
24
  <div
15
25
  v-for="(item, index) in normalizedList"
16
26
  :key="index"
@@ -25,17 +35,19 @@
25
35
  </template>
26
36
 
27
37
  <script setup>
38
+ import QasDialog from '../dialog/QasDialog.vue'
39
+
40
+ import { baseProps } from '../../shared/badge-config'
41
+
28
42
  import {
29
43
  computed,
44
+ nextTick,
30
45
  onMounted,
31
46
  onUnmounted,
32
47
  ref,
33
48
  watch
34
49
  } from 'vue'
35
50
 
36
- import QasDialog from '../dialog/QasDialog.vue'
37
-
38
- // define component name
39
51
  defineOptions({ name: 'QasTextTruncate' })
40
52
 
41
53
  // props
@@ -55,6 +67,11 @@ const props = defineProps({
55
67
  default: ''
56
68
  },
57
69
 
70
+ emptyText: {
71
+ type: String,
72
+ default: '-'
73
+ },
74
+
58
75
  maxWidth: {
59
76
  type: Number,
60
77
  default: 0
@@ -85,13 +102,16 @@ const props = defineProps({
85
102
  default: () => []
86
103
  },
87
104
 
105
+ useBadge: {
106
+ type: Boolean
107
+ },
108
+
88
109
  useObjectList: {
89
110
  type: Boolean
90
111
  },
91
112
 
92
- emptyText: {
93
- type: String,
94
- default: '-'
113
+ useWrapBadge: {
114
+ type: Boolean
95
115
  }
96
116
  })
97
117
 
@@ -100,11 +120,18 @@ const truncate = ref(null)
100
120
  const parent = ref(null)
101
121
 
102
122
  // composable
123
+ const {
124
+ hasBadges,
125
+ badgeParentClasses,
126
+ normalizedBadgesList,
127
+ getBadgeProps
128
+ } = useBadgeHandler()
129
+
103
130
  const {
104
131
  textContent,
105
132
  isTruncated,
106
133
  truncateText
107
- } = useTruncate({ parent, props })
134
+ } = useTruncate({ parent, props, hasBadges })
108
135
 
109
136
  const {
110
137
  defaultProps,
@@ -122,6 +149,7 @@ const {
122
149
 
123
150
  useMutationObserver({ truncate, callbackFn: truncateText })
124
151
 
152
+ // computeds
125
153
  const classes = computed(() => [`text-${props.color}`, `text-${props.typography}`])
126
154
 
127
155
  const formattedText = computed(() => props.list.length || props.text ? displayText.value : props.emptyText)
@@ -182,7 +210,7 @@ function useMutationObserver ({ truncate, callbackFn = () => {} }) {
182
210
  }
183
211
  }
184
212
 
185
- function useTruncate ({ parent, props }) {
213
+ function useTruncate ({ parent, props, hasBadges }) {
186
214
  // reactive vars
187
215
  const maxPossibleWidth = ref('')
188
216
  const textContent = ref('')
@@ -191,14 +219,19 @@ function useTruncate ({ parent, props }) {
191
219
  // lifecycle
192
220
  onMounted(() => truncateText())
193
221
 
194
- // watch
195
- watch(() => props.maxWidth, truncateText)
196
-
197
222
  // computed
198
223
  const isTruncated = computed(() => textWidth.value > maxPossibleWidth.value)
199
224
 
225
+ // watch
226
+ watch(() => props.maxWidth, truncateText)
227
+
200
228
  // functions
201
- function truncateText () {
229
+ async function truncateText () {
230
+ await nextTick()
231
+
232
+ // Se tiver badges, então não pode ser feito calculo de width.
233
+ if (hasBadges.value) return
234
+
202
235
  parent.value.style.maxWidth = '100%'
203
236
  textWidth.value = truncate.value.clientWidth
204
237
  textContent.value = truncate.value?.innerHTML
@@ -271,4 +304,34 @@ function useCounter () {
271
304
  counterLabel
272
305
  }
273
306
  }
307
+
308
+ function useBadgeHandler () {
309
+ const hasBadges = computed(() => props.useBadge && props.useObjectList && props.list.length)
310
+
311
+ const normalizedBadgesList = computed(() => props.list.slice(0, props.maxVisibleItem))
312
+ const badgeParentClasses = computed(() => ({ 'no-wrap': !props.useWrapBadge }))
313
+
314
+ function getBadgeProps (item) {
315
+ const itemProps = {}
316
+
317
+ /**
318
+ * recupera somente keys que estão em baseProps do QasBadge
319
+ * pra evitar que passe propriedades desnecessárias
320
+ */
321
+ for (const key in item) {
322
+ if (baseProps[key]) {
323
+ itemProps[key] = item[key]
324
+ }
325
+ }
326
+
327
+ return itemProps
328
+ }
329
+
330
+ return {
331
+ hasBadges,
332
+ badgeParentClasses,
333
+ normalizedBadgesList,
334
+ getBadgeProps
335
+ }
336
+ }
274
337
  </script>
@@ -18,6 +18,10 @@ props:
18
18
  desc: Seta o título do dialog.
19
19
  type: String
20
20
 
21
+ empty-text:
22
+ desc: Texto a ser exibido no caso de não houver itens no "list" ou o "text" for vazio.
23
+ type: String
24
+
21
25
  max-width:
22
26
  desc: Seta o tamanho máximo do texto.
23
27
  type: Number
@@ -47,13 +51,20 @@ props:
47
51
  default: []
48
52
  type: Array
49
53
 
54
+ use-badge:
55
+ desc: Habilita badges para cada item da lista.
56
+ default: false
57
+ type: Boolean
58
+
50
59
  use-object-list:
51
60
  desc: Utiliza a propriedade "list" como array de objeto contendo label (uso junto a prop list).
61
+ default: false
52
62
  type: Boolean
53
63
 
54
- empty-text:
55
- desc: Texto a ser exibido no caso de não houver itens no "list" ou o "text" for vazio.
56
- type: String
64
+ use-wrap-badge:
65
+ desc: habilita a quebra de linha das badges quando o texto for muito grande.
66
+ default: false
67
+ type: Boolean
57
68
 
58
69
  slots:
59
70
  default:
@@ -3,18 +3,20 @@
3
3
  <q-uploader ref="uploader" auto-upload class="bg-transparent" :class="uploaderClasses" v-bind="attributes" :factory="factory" flat :max-files="maxFiles" method="PUT" @factory-failed="factoryFailed" @uploaded="uploaded" @uploading="updateUploading(true)">
4
4
  <template #header="scope">
5
5
  <slot name="header" :scope="scope">
6
- <div class="flex items-center justify-between">
7
- <div>
8
- <qas-label v-bind="labelProps" />
9
-
10
- <div v-if="errorMessage" class="q-mt-xs text-caption text-negative">
11
- {{ errorMessage }}
6
+ <qas-header class="q-mb-none" v-bind="getHeaderProps(scope)">
7
+ <template #description>
8
+ <div :class="headerDescriptionClasses">
9
+ {{ headerProps.description }}
12
10
  </div>
13
- </div>
11
+ </template>
12
+
13
+ <template v-for="(_, name) in $slots" #[getHeaderSlotName(name)]="context">
14
+ <slot :name v-bind="context" />
15
+ </template>
16
+ </qas-header>
14
17
 
15
- <div v-if="hasAddFile">
16
- <qas-btn color="primary" icon="sym_r_add" :label="addButtonLabel" :use-label-on-small-screen="false" variant="tertiary" @click="onAddButtonClick(scope)" />
17
- </div>
18
+ <div v-if="errorMessage" class="q-mt-xs text-caption text-negative">
19
+ {{ errorMessage }}
18
20
  </div>
19
21
 
20
22
  <!-- ------------------------------------ tags hidden -------------------------------------- -->
@@ -24,15 +26,13 @@
24
26
  </template>
25
27
 
26
28
  <template #list="scope">
27
- <div v-if="hasGalleryCardSection(getFilesList(scope.files, scope))" class="q-col-gutter-lg q-mt-sm row">
29
+ <div v-if="hasGalleryCardSection(getFilesList(scope.files, scope))" class="q-col-gutter-lg row">
28
30
  <div v-for="(file, key, index) in getFilesList(scope.files, scope)" :key="index" :class="columnClasses">
29
31
  <pv-uploader-gallery-card v-bind="getUploaderGalleryCardProps({ key, scope, file, index })" />
30
32
  </div>
31
33
  </div>
32
34
 
33
- <div v-else class="q-mt-lg">
34
- <qas-empty-result-text />
35
- </div>
35
+ <qas-empty-result-text v-else />
36
36
  </template>
37
37
  </q-uploader>
38
38
 
@@ -42,6 +42,7 @@
42
42
 
43
43
  <script>
44
44
  import PvUploaderGalleryCard from './private/PvUploaderGalleryCard.vue'
45
+ import QasHeader from '../header/QasHeader.vue'
45
46
 
46
47
  import { uid, extend } from 'quasar'
47
48
  import { NotifyError } from '../../plugins'
@@ -53,7 +54,8 @@ export default {
53
54
  name: 'QasUploader',
54
55
 
55
56
  components: {
56
- PvUploaderGalleryCard
57
+ PvUploaderGalleryCard,
58
+ QasHeader
57
59
  },
58
60
 
59
61
  inheritAttrs: false,
@@ -125,6 +127,11 @@ export default {
125
127
  type: Object
126
128
  },
127
129
 
130
+ headerProps: {
131
+ type: Object,
132
+ default: () => ({})
133
+ },
134
+
128
135
  label: {
129
136
  type: String,
130
137
  default: ''
@@ -199,6 +206,12 @@ export default {
199
206
  return this.$attrs
200
207
  },
201
208
 
209
+ headerDescriptionClasses () {
210
+ return {
211
+ 'text-negative': this.error
212
+ }
213
+ },
214
+
202
215
  columnClasses () {
203
216
  const irregularClasses = ['col']
204
217
  const columns = this.defaultColumns
@@ -290,15 +303,6 @@ export default {
290
303
  return this.$attrs.multiple || this.$attrs.multiple === ''
291
304
  },
292
305
 
293
- labelProps () {
294
- return {
295
- label: this.label,
296
- margin: 'none',
297
-
298
- ...(this.error && { color: 'negative' })
299
- }
300
- },
301
-
302
306
  self () {
303
307
  return this
304
308
  },
@@ -613,6 +617,48 @@ export default {
613
617
  const emptyModel = this.isMultiple ? [] : this.useObjectModel ? {} : ''
614
618
 
615
619
  this.$emit('update:modelValue', emptyModel)
620
+ },
621
+
622
+ getHeaderSlotName (name) {
623
+ return name.replace('header-', '')
624
+ },
625
+
626
+ getHeaderProps (scope) {
627
+ const { labelProps, actionsMenuProps, ...othersHeaderProps } = this.headerProps
628
+
629
+ const { list, ...othersActionsMenuProps } = actionsMenuProps || {}
630
+
631
+ return {
632
+ spacing: 'lg',
633
+
634
+ labelProps: {
635
+ label: this.label,
636
+
637
+ margin: 'none',
638
+
639
+ ...labelProps,
640
+
641
+ ...(this.error && { color: 'negative' })
642
+ },
643
+
644
+ ...(this.hasAddFile && {
645
+ actionsMenuProps: {
646
+ list: {
647
+ add: {
648
+ icon: 'sym_r_add',
649
+ label: this.addButtonLabel,
650
+ handler: () => this.onAddButtonClick(scope)
651
+ },
652
+
653
+ ...list
654
+ },
655
+
656
+ ...othersActionsMenuProps
657
+ }
658
+ }),
659
+
660
+ ...othersHeaderProps
661
+ }
616
662
  }
617
663
  }
618
664
  }
@@ -83,8 +83,13 @@ props:
83
83
  default: {}
84
84
  type: Object
85
85
 
86
+ header-props:
87
+ desc: Propriedades repassadas para o componente "QasHeader".
88
+ default: {}
89
+ type: Object
90
+
86
91
  label:
87
- desc: Label do componente, repassado para o componente "QasLabel".
92
+ desc: Label do componente.
88
93
  type: String
89
94
 
90
95
  max-files:
@@ -158,6 +163,15 @@ slots:
158
163
  default: {}
159
164
  type: Object
160
165
 
166
+ header-actions:
167
+ desc: Acessa o slot actions do "QasHeader".
168
+
169
+ header-description:
170
+ desc: Acessa o slot description do "QasHeader".
171
+
172
+ header-label:
173
+ desc: Acessa o slot label do "QasHeader".
174
+
161
175
  list:
162
176
  desc: Acesso ao conteúdo onde fica a listagem de arquivos.
163
177
  scope:
@@ -14,6 +14,10 @@
14
14
  </slot>
15
15
  </div>
16
16
 
17
+ <div v-if="hasAfterGreetingSlot" class="q-mt-lg">
18
+ <slot name="after-greeting" />
19
+ </div>
20
+
17
21
  <div v-if="hasShortcuts">
18
22
  <qas-label class="q-mt-lg" label="Atalhos" />
19
23
 
@@ -102,6 +106,10 @@ export default {
102
106
  if (time >= '12:00' && time < '18:59') return 'Boa tarde'
103
107
 
104
108
  return 'Boa noite'
109
+ },
110
+
111
+ hasAfterGreetingSlot () {
112
+ return !!this.$slots['after-greeting']
105
113
  }
106
114
  }
107
115
  }
@@ -21,3 +21,6 @@ props:
21
21
  slots:
22
22
  actions:
23
23
  desc: Slot para substituir o QasActionsMenu.
24
+
25
+ after-greeting:
26
+ desc: Slot para acessar o conteúdo depois da saudação.
@@ -1,8 +1,10 @@
1
1
  export { default as useContext } from './use-context.js'
2
+ export { default as useDefaultFilters } from './use-default-filters.js'
2
3
  export { default as useForm } from './use-form.js'
3
4
  export { default as useHistory } from './use-history.js'
5
+ export { default as useNotifications } from './use-notifications.js'
4
6
  export { default as useQueryCache } from './use-query-cache.js'
5
7
  export { default as useScreen } from './use-screen.js'
6
- export { default as useNotifications } from './use-notifications.js'
7
8
 
8
9
  export * from './use-notifications.js'
10
+ export * from './use-default-filters.js'
@@ -1,3 +1,4 @@
1
1
  export { default as useView } from './use-view'
2
2
  export { default as useGenerator } from './use-generator'
3
3
  export { default as useToggleVisibility } from './use-toggle-visibility'
4
+ export { default as useAuthUser } from './use-auth-user'
@@ -0,0 +1,20 @@
1
+ import { LocalStorage } from 'quasar'
2
+
3
+ import { computed, ref } from 'vue'
4
+
5
+ const user = ref(LocalStorage.getItem('user'))
6
+
7
+ export default function useAuthUser () {
8
+ window.addEventListener('message', ({ data }) => {
9
+ if (data.type !== 'updateUser') return
10
+
11
+ user.value = data.user
12
+ })
13
+
14
+ const hasUser = computed(() => !!user.value)
15
+
16
+ return {
17
+ user,
18
+ hasUser
19
+ }
20
+ }
@@ -1,6 +1,8 @@
1
- import { computed } from 'vue'
2
1
  import { Spacing } from '../../enums/Spacing'
3
2
  import { gutterValidator } from '../../helpers/private/gutter-validator'
3
+ import useScreen from '../use-screen'
4
+
5
+ import { computed } from 'vue'
4
6
 
5
7
  const IRREGULAR_CLASSES = ['col', 'col-auto', 'fit']
6
8
 
@@ -21,7 +23,7 @@ export const baseProps = {
21
23
  },
22
24
 
23
25
  gutter: {
24
- default: Spacing.Md,
26
+ default: undefined,
25
27
  type: [String, Boolean],
26
28
  validator: gutterValidator
27
29
  }
@@ -34,20 +36,33 @@ export const baseProps = {
34
36
  * @name useGenerator
35
37
  * @param {Object} options - Opções do componente.
36
38
  * @param {baseProps} options.props - Propriedades do componente.
39
+ * @param {boolean} options.isGrid - Propriedades do componente.
37
40
  * @returns {{
38
41
  * classes: classes,
39
42
  * getFieldClass: getFieldClass
40
43
  * }}
41
44
  */
42
- export default function ({ props = {} }) {
45
+ export default function ({ props = {}, isGrid = false }) {
46
+ const screen = useScreen()
47
+
48
+ /**
49
+ * Se a propriedade gutter não for passada, será calculada automaticamente.
50
+ * se for usado no grid e for inline, o gutter será menor.
51
+ */
52
+ const defaultGutter = computed(() => {
53
+ if (props.gutter !== undefined) return props.gutter
54
+
55
+ return isGrid && (props.useInline && !screen.isSmall) ? Spacing.Sm : Spacing.Md
56
+ })
57
+
43
58
  /**
44
59
  * @type {{ value: string[] | string }}
45
60
  */
46
61
  const classes = computed(() => {
47
62
  const classesList = ['row']
48
63
 
49
- if (props.gutter) {
50
- classesList.push(`q-col-gutter-${props.gutter}`)
64
+ if (defaultGutter.value) {
65
+ classesList.push(`q-col-gutter-${defaultGutter.value}`)
51
66
  }
52
67
 
53
68
  return classesList
@@ -0,0 +1,106 @@
1
+ import { computed, ref } from 'vue'
2
+ import { LocalStorage, is } from 'quasar'
3
+
4
+ const filterQuery = ref({})
5
+
6
+ const defaultFiltersHooks = {
7
+ defaultFiltersChange: []
8
+ }
9
+
10
+ /**
11
+ * Define os filtros padrão antes de entrar na rota.
12
+ *
13
+ * Prioridade de aplicação dos filtros:
14
+ * 1 - Se o filtro estiver salvo no estado global, então utiliza ele.
15
+ * 2 - Se o filtro já estiver na query, então utiliza ele.
16
+ * 3 - Se o filtro já estiver salvo no LocalStorage, então utiliza ele.
17
+ *
18
+ * @param {Object} to - Rota de destino.
19
+ * @param {Object} _from - Rota de origem.
20
+ * @param {Function} next - Função de redirecionamento.
21
+ * @param {Array} queryList='company' - Lista de filtros a serem aplicados.
22
+ */
23
+ export function setDefaultFiltersBeforeEnter (to, _from, next, queryList = ['company']) {
24
+ const { getDefaultFiltersFromStorage, setFilterQuery } = useDefaultFilters()
25
+
26
+ const { query } = to
27
+ const newQuery = { ...query }
28
+
29
+ // recupera os filtros padrão do LocalStorage
30
+ const defaultFiltersFromStorage = getDefaultFiltersFromStorage()
31
+
32
+ queryList.forEach(name => {
33
+ // 1. se o filtro já estiver salvo no estado, então utiliza ele
34
+ if (filterQuery.value[name]) {
35
+ newQuery[name] = filterQuery.value[name]
36
+ return
37
+ }
38
+
39
+ // 2. se o filtro já estiver na query, então utiliza ele
40
+ if (query[name]) {
41
+ setFilterQuery(query[name], name)
42
+
43
+ return
44
+ }
45
+
46
+ const storedFilter = defaultFiltersFromStorage[name]
47
+
48
+ // 3. se o filtro já estiver salvo no LocalStorage, então utiliza ele
49
+ if (storedFilter) {
50
+ setFilterQuery(storedFilter, name)
51
+ newQuery[name] = storedFilter
52
+ }
53
+ })
54
+
55
+ /**
56
+ * Verifica se houve mudanças na query antes de redirecionar, sem essa validação
57
+ * o redirecionamento ocorre mesmo que a query seja a mesma, gerando loop infinito.
58
+ */
59
+ if (!is.deepEqual(newQuery, query)) return next({ ...to, query: newQuery, replace: true })
60
+
61
+ next()
62
+ }
63
+
64
+ /**
65
+ * Este composable recupera os filtros default salvos no LocalStorage na chave 'defaultFilters'.
66
+ */
67
+ export default function useDefaultFilters () {
68
+ const hasFilterQuery = computed(() => !!Object.keys(filterQuery.value).length)
69
+
70
+ function setFilterQuery (query, name = 'company') {
71
+ filterQuery.value[name] = query
72
+ }
73
+
74
+ function getDefaultFiltersFromStorage () {
75
+ const defaultFiltersFromStorage = LocalStorage.getItem('defaultFilters') || {}
76
+
77
+ return defaultFiltersFromStorage
78
+ }
79
+
80
+ function triggerDefaultFiltersChange (newFilters, oldFilters) {
81
+ // necessário verificar se houve mudanças antes de disparar o evento para não duplicar.
82
+ if (is.deepEqual(newFilters, oldFilters)) return
83
+
84
+ defaultFiltersHooks.defaultFiltersChange.forEach(hook => hook(newFilters, oldFilters))
85
+ }
86
+
87
+ function onDefaultFiltersChange (callback) {
88
+ defaultFiltersHooks.defaultFiltersChange.push(callback)
89
+ }
90
+
91
+ function removeOnDefaultFiltersChange (callback) {
92
+ const index = defaultFiltersHooks.defaultFiltersChange.indexOf(callback)
93
+
94
+ if (~index) defaultFiltersHooks.defaultFiltersChange.splice(index, 1)
95
+ }
96
+
97
+ return {
98
+ filterQuery,
99
+ hasFilterQuery,
100
+ getDefaultFiltersFromStorage,
101
+ onDefaultFiltersChange,
102
+ removeOnDefaultFiltersChange,
103
+ setFilterQuery,
104
+ triggerDefaultFiltersChange
105
+ }
106
+ }
@@ -4,6 +4,8 @@ import hasParentByClassName from '../helpers/private/has-parent-by-class-name'
4
4
  import { Notify } from 'quasar'
5
5
  import { ref } from 'vue'
6
6
 
7
+ import naveNotificationSound from '../assets/sounds/nave-notification.mp3'
8
+
7
9
  const callbackFunctions = {
8
10
  onNotificationReceived: []
9
11
  }
@@ -63,6 +65,8 @@ export default function () {
63
65
  timeout: 30000
64
66
  })
65
67
 
68
+ sendNotificationSound()
69
+
66
70
  /**
67
71
  * Função que é chamada quando o usuário clica na notificação, se a notificação
68
72
  * tem link, então ele vai ser redirecionado para o link em uma nova aba, caso
@@ -96,6 +100,16 @@ export default function () {
96
100
  </div>
97
101
  `)
98
102
  }
103
+
104
+ /**
105
+ * Função que toca o som de notificação.
106
+ */
107
+ function sendNotificationSound () {
108
+ const audio = new Audio(naveNotificationSound)
109
+
110
+ // o áudio agora é reproduzível; reproduza-o se as permissões permitirem
111
+ audio.addEventListener('canplaythrough', audio.play)
112
+ }
99
113
  }
100
114
 
101
115
  return {
@@ -19,7 +19,7 @@ export default function () {
19
19
  }
20
20
 
21
21
  function findOne (key, filter) {
22
- return cachedFilters[key][filter]
22
+ return cachedFilters[key]?.[filter]
23
23
  }
24
24
 
25
25
  function findAll (key) {