@bildvitta/quasar-ui-asteroid 3.16.0-beta.1 → 3.16.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/package.json +2 -1
  2. package/src/components/actions/QasActions.vue +7 -0
  3. package/src/components/actions-menu/QasActionsMenu.vue +3 -2
  4. package/src/components/app-menu/private/PvAppMenuHelpChat.vue +4 -2
  5. package/src/components/btn-dropdown/QasBtnDropdown.vue +3 -3
  6. package/src/components/expansion-item/QasExpansionItem.vue +117 -12
  7. package/src/components/expansion-item/QasExpansionItem.yml +8 -0
  8. package/src/components/form-view/QasFormView.vue +3 -1
  9. package/src/components/form-view/QasFormView.yml +3 -0
  10. package/src/components/grid-generator/QasGridGenerator.vue +2 -10
  11. package/src/components/header-actions/QasHeaderActions.vue +17 -4
  12. package/src/components/header-actions/QasHeaderActions.yml +6 -0
  13. package/src/components/label/QasLabel.vue +2 -2
  14. package/src/components/list-view/QasListView.vue +3 -1
  15. package/src/components/list-view/QasListView.yml +3 -0
  16. package/src/components/single-view/QasSingleView.vue +132 -81
  17. package/src/components/single-view/QasSingleView.yml +8 -0
  18. package/src/components/table-generator/QasTableGenerator.vue +16 -2
  19. package/src/components/table-generator/QasTableGenerator.yml +5 -0
  20. package/src/components/toggle-visibility/QasToggleVisibility.vue +79 -0
  21. package/src/components/toggle-visibility/QasToggleVisibility.yml +30 -0
  22. package/src/composables/private/index.js +3 -0
  23. package/src/composables/private/use-generator.js +9 -3
  24. package/src/composables/private/use-toggle-visibility.js +48 -0
  25. package/src/composables/private/use-view.js +186 -0
  26. package/src/mixins/view.js +12 -1
  27. package/src/vue-plugin.js +6 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bildvitta/quasar-ui-asteroid",
3
3
  "description": "Asteroid",
4
- "version": "3.16.0-beta.1",
4
+ "version": "3.16.0-beta.10",
5
5
  "author": "Bild & Vitta <systemteam@bild.com.br>",
6
6
  "license": "MIT",
7
7
  "main": "dist/asteroid.cjs.min.js",
@@ -43,6 +43,7 @@
43
43
  "sass": "^1.62.1"
44
44
  },
45
45
  "dependencies": {
46
+ "@bildvitta/composables": "^1.0.0-beta.7",
46
47
  "@bildvitta/store-adapter": "^1.0.0",
47
48
  "autonumeric": "^4.9.0",
48
49
  "axios": "^1.4.0",
@@ -1,5 +1,9 @@
1
1
  <template>
2
2
  <div class="q-mt-sm" :class="classes">
3
+ <div v-if="hasTertiarySlot" :class="columnClasses">
4
+ <slot name="tertiary" />
5
+ </div>
6
+
3
7
  <div v-if="hasSecondarySlot" :class="columnClasses">
4
8
  <slot name="secondary" />
5
9
  </div>
@@ -52,6 +56,8 @@ const classes = computed(() => {
52
56
  const isSmallOrFullWidth = screen.isSmall || props.useFullWidth
53
57
 
54
58
  return [
59
+ 'items-center',
60
+
55
61
  // alinhamento
56
62
  `justify-${props.align}`,
57
63
 
@@ -71,4 +77,5 @@ const columnClasses = computed(() => {
71
77
 
72
78
  const hasPrimarySlot = computed(() => !!slots.primary)
73
79
  const hasSecondarySlot = computed(() => !!slots.secondary)
80
+ const hasTertiarySlot = computed(() => !!slots.tertiary)
74
81
  </script>
@@ -3,7 +3,7 @@
3
3
  <qas-btn-dropdown v-bind="btnDropdownProps">
4
4
  <q-list data-cy="actions-menu-list">
5
5
  <slot v-for="(item, key) in formattedList.dropdownList" :item="item" :name="key">
6
- <q-item v-bind="getItemProps(item)" :key="key" clickable data-cy="actions-menu-list-item" @click="setClickHandler(item)">
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-icon :name="item.icon" />
9
9
  </q-item-section>
@@ -240,10 +240,11 @@ const { showTooltip, tooltipLabels } = useTooltips({ formattedList, fullList, pr
240
240
 
241
241
  // functions
242
242
  function getItemProps (item) {
243
- const { disable, props: itemProps } = item
243
+ const { disable, props: itemProps, to } = item
244
244
 
245
245
  return {
246
246
  disable,
247
+ to,
247
248
  ...itemProps
248
249
  }
249
250
  }
@@ -106,10 +106,12 @@ onUnmounted(() => {
106
106
  * Define o estilo do iframe de acordo com a posição do "chatContent" dentro
107
107
  * do QMenu.
108
108
  */
109
- function setIframeStyle () {
109
+ async function setIframeStyle () {
110
+ await nextTick()
111
+
110
112
  const iframe = getIframe()
111
113
 
112
- if (!iframe) return
114
+ if (!iframe || !chatContent.value) return
113
115
 
114
116
  const { bottom, left, top } = chatContent.value.getBoundingClientRect()
115
117
 
@@ -2,8 +2,8 @@
2
2
  <div class="qas-btn-dropdown" :class="classes.parent">
3
3
  <div v-if="hasButtons" :class="classes.list">
4
4
  <div v-for="(buttonProps, key, index) in props.buttonsPropsList" :key="key">
5
- <div class="flex">
6
- <qas-btn :disable="props.disable" v-bind="buttonProps" variant="tertiary" @click="onClick">
5
+ <div class="flex no-wrap">
6
+ <qas-btn :disable="props.disable" v-bind="buttonProps" no-wrap variant="tertiary" @click="onClick">
7
7
  <q-menu v-if="hasMenuOnLeftSide" v-model="isMenuOpened" anchor="bottom right" auto-close self="top right" @update:model-value="onUpdateMenuValue">
8
8
  <div :class="classes.menuContent">
9
9
  <slot />
@@ -91,7 +91,7 @@ const classes = computed(() => {
91
91
  },
92
92
 
93
93
  list: {
94
- flex: !isSingleButton.value
94
+ 'flex no-wrap': !isSingleButton.value
95
95
  }
96
96
  }
97
97
  })
@@ -1,13 +1,13 @@
1
1
  <template>
2
- <div class="qas-expansion-item" :class="errorClasses">
3
- <qas-box class="qas-expansion-item__box">
4
- <q-expansion-item header-class="text-bold q-mt-sm q-pa-none" :label="props.label">
2
+ <div ref="expansionItem" class="full-width qas-expansion-item" :class="errorClasses" v-bind="expansionProps.parent">
3
+ <component :is="component.is" class="qas-expansion-item__box">
4
+ <q-expansion-item header-class="text-bold q-mt-sm q-pa-none qas-expansion-item__header" :label="props.label" v-bind="expansionProps.item">
5
5
  <template #header>
6
6
  <slot name="header">
7
7
  <div class="full-width">
8
8
  <div class="items-center q-col-gutter-sm row">
9
9
  <slot name="label">
10
- <h5 class="col-auto text-h5 text-weight-medium">
10
+ <h5 class="col-auto text-h5">
11
11
  {{ props.label }}
12
12
  </h5>
13
13
  </slot>
@@ -18,17 +18,25 @@
18
18
  </div>
19
19
  </div>
20
20
  </div>
21
+
22
+ <div v-if="hasHeaderBottom" class="q-mt-sm">
23
+ <slot name="header-bottom" />
24
+ </div>
21
25
  </div>
22
26
  </slot>
23
27
  </template>
24
28
 
25
- <q-separator class="q-my-md" />
29
+ <q-separator v-if="hasHeaderSeparator" class="q-my-md" />
30
+
31
+ <div :class="contentClasses">
32
+ <slot name="content">
33
+ <qas-grid-generator v-if="hasGridGenerator" use-inline v-bind="gridGeneratorProps" />
34
+ </slot>
35
+ </div>
26
36
 
27
- <slot name="content">
28
- <qas-grid-generator v-if="hasGridGenerator" v-bind="gridGeneratorProps" use-inline />
29
- </slot>
37
+ <q-separator v-if="hasBottomSeparator" class="q-mt-md" />
30
38
  </q-expansion-item>
31
- </qas-box>
39
+ </component>
32
40
 
33
41
  <div v-if="hasError" class="q-pt-sm qas-expansion-item__error-message text-caption text-negative">
34
42
  {{ props.errorMessage }}
@@ -37,9 +45,14 @@
37
45
  </template>
38
46
 
39
47
  <script setup>
40
- import { computed } from 'vue'
48
+ import QasBox from '../box/QasBox.vue'
41
49
 
42
- defineOptions({ name: 'QasExpansionItem' })
50
+ import { computed, provide, inject, onMounted, ref, useAttrs } from 'vue'
51
+
52
+ defineOptions({
53
+ name: 'QasExpansionItem',
54
+ inheritAttrs: false
55
+ })
43
56
 
44
57
  const props = defineProps({
45
58
  badges: {
@@ -64,20 +77,112 @@ const props = defineProps({
64
77
  gridGeneratorProps: {
65
78
  type: Object,
66
79
  default: () => ({})
80
+ },
81
+
82
+ useHeaderSeparator: {
83
+ type: Boolean,
84
+ default: undefined
67
85
  }
68
86
  })
69
87
 
88
+ const attrs = useAttrs()
89
+
90
+ provide('isExpansionItem', true)
91
+
92
+ // slots
93
+ const slots = defineSlots()
94
+
95
+ // refs
96
+ const expansionItem = ref(null)
97
+ const hasNextSibling = ref(false)
98
+
99
+ onMounted(setHasNextSibling)
100
+
101
+ // constants
102
+ const isNestedExpansionItem = inject('isExpansionItem', false)
103
+ const component = {
104
+ is: isNestedExpansionItem ? 'div' : QasBox
105
+ }
106
+
107
+ // computed
70
108
  const hasError = computed(() => props.error || !!props.errorMessage)
109
+ const hasGridGenerator = computed(() => !!Object.keys(props.gridGeneratorProps).length)
110
+ const hasBottomSeparator = computed(() => isNestedExpansionItem && hasNextSibling.value)
111
+ const hasHeaderBottom = computed(() => !!slots['header-bottom'])
112
+
113
+ /**
114
+ * Verifica se o componente deve adicionar um separador no header.
115
+ *
116
+ * - Se a propriedade useHeaderSeparator for true, retorna separador.
117
+ * - Se a propriedade useHeaderSeparator for undefined, retorna separador apenas se não for um componente aninhado.
118
+ * - Se a propriedade useHeaderSeparator for false, não retorna separador.
119
+ */
120
+ const hasHeaderSeparator = computed(() => {
121
+ return typeof props.useHeaderSeparator === 'undefined' ? !isNestedExpansionItem : props.useHeaderSeparator
122
+ })
71
123
 
72
124
  const errorClasses = computed(() => ({ 'qas-expansion-item--error': hasError.value }))
73
125
 
74
- const hasGridGenerator = computed(() => !!Object.keys(props.gridGeneratorProps).length)
126
+ const contentClasses = computed(() => {
127
+ return {
128
+ 'q-mt-sm': isNestedExpansionItem,
129
+ 'q-mt-md': !isNestedExpansionItem && !props.useHeaderSeparator
130
+ }
131
+ })
132
+
133
+ const expansionProps = computed(() => {
134
+ const {
135
+ 'onUpdate:modelValue': onUpdateModelValue,
136
+ onShow,
137
+ onBeforeShow,
138
+ onBeforeHide,
139
+ onAfterShow,
140
+ onAfterHide,
141
+ ...propsPayload
142
+ } = attrs
143
+
144
+ return {
145
+ parent: {
146
+ ...propsPayload
147
+ },
148
+
149
+ item: {
150
+ onUpdateModelValue,
151
+ onShow,
152
+ onBeforeShow,
153
+ onBeforeHide,
154
+ onAfterShow,
155
+ onAfterHide
156
+ }
157
+ }
158
+ })
159
+
160
+ // functions
161
+
162
+ /**
163
+ * Caso o componente esteja dentro de um QasExpansionItem, verifica se existe um próximo irmão
164
+ * para adicionar um separador.
165
+ */
166
+ function setHasNextSibling (value) {
167
+ if (!isNestedExpansionItem) return
168
+
169
+ const hasTextContentSibling = !!expansionItem.value.nextSibling?.textContent?.trim?.()
170
+ const hasElementSibling = !!expansionItem.value.nextElementSibling
171
+
172
+ hasNextSibling.value = hasElementSibling || hasTextContentSibling
173
+ }
75
174
  </script>
76
175
 
77
176
  <style lang="scss">
78
177
  .qas-expansion-item {
79
178
  $root: &;
80
179
 
180
+ // em alguns casos quando usado com grid, o espaçamento afetava o header, com z-index o problema é resolvido
181
+ &__header {
182
+ position: relative;
183
+ z-index: 1;
184
+ }
185
+
81
186
  &--error {
82
187
  #{$root}__box {
83
188
  border: 2px solid $negative;
@@ -24,10 +24,18 @@ props:
24
24
  desc: Propriedades que serão repassadas para o QasGridGenerator.
25
25
  type: Object
26
26
 
27
+ use-header-separator:
28
+ desc: Propriedade para forçar o QSeparator no header.
29
+ type: Boolean
30
+ default: undefined
31
+
27
32
  slots:
28
33
  header:
29
34
  desc: 'Slot para substituir o conteúdo do header'
30
35
 
36
+ header-bottom:
37
+ desc: 'Slot para acessar o conteúdo que fica abaixo do header.'
38
+
31
39
  label:
32
40
  desc: 'Slot para substituir o conteúdo da label do header.'
33
41
 
@@ -5,7 +5,9 @@
5
5
  </header>
6
6
 
7
7
  <q-form ref="form" v-bind="defaultFormProps">
8
- <slot />
8
+ <slot v-if="mx_canShowFetchErrorSlot" name="fetch-error" />
9
+
10
+ <slot v-else />
9
11
 
10
12
  <slot v-if="useActions" name="actions">
11
13
  <qas-actions>
@@ -155,6 +155,9 @@ slots:
155
155
  default:
156
156
  desc: Slot para ter o conteúdo principal (dentro do main).
157
157
 
158
+ fetch-error:
159
+ desc: Slot disponibilizado em caso de fetchError.
160
+
158
161
  footer:
159
162
  desc: Slot para acessar o footer.
160
163
 
@@ -70,7 +70,7 @@ const props = defineProps({
70
70
  })
71
71
 
72
72
  // composables
73
- const { classes: useGeneratorClasses, getFieldClass } = useGenerator({ props })
73
+ const { classes, getFieldClass } = useGenerator({ props })
74
74
 
75
75
  // computed
76
76
  const hasResult = computed(() => Object.keys(props.result).length)
@@ -97,12 +97,6 @@ const headerClass = computed(() => {
97
97
  ]
98
98
  })
99
99
 
100
- const classes = computed(() => {
101
- if (props.useInline) return 'row q-col-gutter-md'
102
-
103
- return useGeneratorClasses.value
104
- })
105
-
106
100
  const fieldsByResult = ref({})
107
101
 
108
102
  /**
@@ -163,9 +157,7 @@ function setFieldsByResult () {
163
157
  }
164
158
 
165
159
  function getContainerClass ({ key }) {
166
- if (props.useInline) {
167
- return 'row justify-between col-12'
168
- }
160
+ if (props.useInline) return 'row justify-between col-12'
169
161
 
170
162
  return getFieldClass({ index: key, isGridGenerator: true })
171
163
  }
@@ -1,12 +1,12 @@
1
1
  <template>
2
- <div class="justify-between no-wrap q-col-gutter-x-md q-mb-xl row text-body1 text-grey-8" :class="containerClass">
2
+ <div class="justify-between no-wrap q-col-gutter-x-md row text-body1 text-grey-8" :class="containerClass">
3
3
  <div :class="leftClass">
4
4
  <slot name="left">
5
5
  {{ props.text }}
6
6
  </slot>
7
7
  </div>
8
8
 
9
- <div v-if="hasRightSide" class="col-3 col-md-3 col-sm-4 justify-end row">
9
+ <div v-if="hasRightSide" class="justify-end row">
10
10
  <slot name="right">
11
11
  <qas-actions-menu v-if="hasDefaultActionsMenu" v-bind="props.actionsMenuProps" />
12
12
 
@@ -18,6 +18,8 @@
18
18
 
19
19
  <script setup>
20
20
  import { FlexAlign } from '../../enums/Align'
21
+ import { Spacing } from '../../enums/Spacing'
22
+ import { gutterValidator } from '../../helpers/private/gutter-validator'
21
23
 
22
24
  import { computed, useSlots } from 'vue'
23
25
 
@@ -43,14 +45,25 @@ const props = defineProps({
43
45
  text: {
44
46
  type: String,
45
47
  default: ''
48
+ },
49
+
50
+ spacing: {
51
+ default: Spacing.Xl,
52
+ type: String,
53
+ validator: gutterValidator
46
54
  }
47
55
  })
48
56
 
49
57
  const slots = useSlots()
50
58
 
51
59
  // computed
52
- const containerClass = computed(() => `items-${props.alignColumns}`)
53
- const leftClass = computed(() => hasRightSide.value ? 'col-9 col-md-9 col-sm-8' : 'col-12')
60
+ const containerClass = computed(() => `items-${props.alignColumns} q-mb-${props.spacing}`)
61
+
62
+ const leftClass = computed(() => {
63
+ return {
64
+ 'col-12': !hasRightSide.value
65
+ }
66
+ })
54
67
 
55
68
  const hasDefaultButton = computed(() => !!Object.keys(props.buttonProps).length)
56
69
  const hasDefaultActionsMenu = computed(() => !!Object.keys(props.actionsMenuProps).length)
@@ -24,6 +24,12 @@ props:
24
24
  desc: Descrição da seção a esquerda.
25
25
  type: String
26
26
 
27
+ spacing:
28
+ desc: Espaçamento entre o componente e o conteúdo abaixo.
29
+ default: xl
30
+ type: String
31
+ examples: [none, xs, sm, md, lg, xl, '2xl', '3xl', '4xl', '5xl']
32
+
27
33
  slots:
28
34
  left:
29
35
  desc: slot para acessar seção da esquerda (descrição).
@@ -1,7 +1,7 @@
1
1
  <template>
2
- <div :class="classes">
2
+ <component :is="props.typography" :class="classes">
3
3
  <slot :label-with-suffix="formattedLabel">{{ formattedLabel }}</slot>
4
- </div>
4
+ </component>
5
5
  </template>
6
6
 
7
7
  <script setup>
@@ -15,7 +15,9 @@
15
15
  </div>
16
16
 
17
17
  <div v-else-if="!mx_isFetching">
18
- <slot name="empty-results">
18
+ <slot v-if="mx_canShowFetchErrorSlot" name="fetch-error" />
19
+
20
+ <slot v-else name="empty-results">
19
21
  <qas-empty-result-text />
20
22
  </slot>
21
23
  </div>
@@ -97,6 +97,9 @@ slots:
97
97
  empty-results:
98
98
  desc: 'Slot acessar quando a listagem está vazia.'
99
99
 
100
+ fetch-error:
101
+ desc: Slot disponibilizado em caso de fetchError.
102
+
100
103
  filter:
101
104
  desc: 'Slot para acessar o filtro.'
102
105
 
@@ -1,124 +1,175 @@
1
1
  <template>
2
- <div :class="mx_componentClass">
3
- <header v-if="mx_hasHeaderSlot">
2
+ <div :class="componentClass">
3
+ <header v-if="hasHeaderSlot">
4
4
  <slot name="header" />
5
5
  </header>
6
6
 
7
- <template v-if="hasResult">
7
+ <slot v-if="canShowFetchErrorSlot" name="fetch-error" />
8
+
9
+ <template v-else-if="hasResult">
8
10
  <slot />
9
11
  </template>
10
12
 
11
- <qas-empty-result-text v-else-if="!mx_isFetching" class="q-my-xl" />
13
+ <div v-else-if="!viewState.fetching">
14
+ <qas-empty-result-text class="q-my-xl" />
15
+ </div>
12
16
 
13
- <footer v-if="mx_hasFooterSlot">
17
+ <footer v-if="hasFooterSlot">
14
18
  <slot name="footer" />
15
19
  </footer>
16
20
 
17
- <q-inner-loading :showing="mx_isFetching">
21
+ <q-inner-loading :showing="viewState.fetching">
18
22
  <q-spinner color="grey" size="3em" />
19
23
  </q-inner-loading>
20
24
  </div>
21
25
  </template>
22
26
 
23
- <script>
24
- import viewMixin from '../../mixins/view'
27
+ <script setup>
28
+ import useView, { baseProps, baseEmits } from '../../composables/private/use-view'
25
29
 
26
- import { markRaw } from 'vue'
27
- import { getGetter, getAction } from '@bildvitta/store-adapter'
28
30
  import debug from 'debug'
31
+ import { decamelize } from 'humps'
32
+ import { markRaw, computed, watch, inject } from 'vue'
33
+ import { useRoute } from 'vue-router'
29
34
 
30
35
  const log = debug('asteroid-ui:qas-single-view')
31
36
 
32
- export default {
33
- name: 'QasSingleView',
34
-
35
- mixins: [viewMixin],
37
+ defineOptions({ name: 'QasSingleView' })
36
38
 
37
- props: {
38
- customId: {
39
- default: '',
40
- type: [Number, String]
41
- },
39
+ const props = defineProps({
40
+ ...baseProps,
42
41
 
43
- result: {
44
- default: () => ({}),
45
- type: Object
46
- }
42
+ customId: {
43
+ default: '',
44
+ type: [Number, String]
47
45
  },
48
46
 
49
- emits: [
50
- 'update:result',
51
- 'fetch-success',
52
- 'fetch-error'
53
- ],
47
+ result: {
48
+ default: () => ({}),
49
+ type: Object
50
+ }
51
+ })
54
52
 
55
- computed: {
56
- hasResult () {
57
- return !!this.resultModel
58
- },
53
+ const emit = defineEmits([
54
+ ...baseEmits,
59
55
 
60
- id () {
61
- return this.customId || this.$route.params.id
62
- },
56
+ 'update:result',
57
+ 'fetch-success',
58
+ 'fetch-error'
59
+ ])
63
60
 
64
- resultModel () {
65
- return getGetter.call(this, { entity: this.entity, key: 'byId' })(this.id) || {}
66
- }
67
- },
61
+ const slots = defineSlots()
68
62
 
69
- watch: {
70
- $route (to, from) {
71
- if (to.name === from.name) {
72
- this.mx_fetchHandler({ id: this.id, url: this.url }, this.fetchSingle)
73
- }
74
- },
63
+ // globals
64
+ const axios = inject('axios')
65
+ const qas = inject('qas')
75
66
 
76
- resultModel (value) {
77
- this.$emit('update:result', markRaw({ ...value }))
78
- }
79
- },
67
+ // composables
68
+ const route = useRoute()
80
69
 
81
- created () {
82
- this.mx_fetchHandler({ id: this.id, url: this.url }, this.fetchSingle)
83
- },
70
+ const {
71
+ // state
72
+ viewState,
73
+
74
+ // computed
75
+ componentClass,
76
+ hasHeaderSlot,
77
+ hasFooterSlot,
78
+ canShowFetchErrorSlot,
79
+
80
+ // functions
81
+ errorHandler,
82
+ fetchHandler,
83
+ setErrors,
84
+ setFields,
85
+ setMetadata,
86
+ setResult,
87
+ updateModels
88
+ } = useView({ emit, props, slots, mode: 'single' })
89
+
90
+ // Expose
91
+ defineExpose({ fetchSingle, fetchHandler })
92
+
93
+ // computed
94
+ const id = computed(() => props.customId || route.params.id)
95
+
96
+ const resultModel = computed(() => {
97
+ if (props.useStore) return qas.getGetter({ entity: props.entity, key: 'byId' })(id.value) || {}
98
+
99
+ return viewState.value.result || {}
100
+ })
101
+
102
+ const hasResult = computed(() => !!resultModel.value)
84
103
 
85
- methods: {
86
- async fetchSingle (externalPayload = {}) {
87
- this.mx_isFetching = true
104
+ // watch
105
+ watch(() => route, (to, from) => {
106
+ if (to.name === from.name) {
107
+ fetchHandler({ id: id.value, url: props.url }, fetchSingle)
108
+ }
109
+ })
110
+
111
+ watch(() => resultModel.value, value => emit('update:result', markRaw({ ...value })))
112
+
113
+ // created
114
+ fetchHandler({ id: id.value, url: props.url }, fetchSingle)
115
+
116
+ // functions
117
+ async function fetchSingle (externalPayload = {}) {
118
+ viewState.value.fetching = true
88
119
 
89
- try {
90
- const payload = { id: this.id, url: this.url, ...externalPayload }
120
+ try {
121
+ const payload = { id: id.value, url: props.url, ...externalPayload }
91
122
 
92
- const response = await getAction.call(this, {
93
- entity: this.entity,
94
- key: 'fetchSingle',
95
- payload
96
- })
123
+ const response = await getAction(payload)
97
124
 
98
- const { errors, fields, metadata } = response.data
125
+ const { errors, fields, metadata, result } = response.data
99
126
 
100
- this.mx_setErrors(errors)
101
- this.mx_setFields(fields)
102
- this.mx_setMetadata(metadata)
127
+ setErrors(errors)
128
+ setFields(fields)
129
+ setMetadata(metadata)
103
130
 
104
- this.mx_updateModels({
105
- errors: this.mx_errors,
106
- fields: this.mx_fields,
107
- metadata: this.mx_metadata
108
- })
131
+ /**
132
+ * result só precisa ser adicionado ao estado se não estiver usando store,
133
+ * pois com a store é usado o getter.
134
+ */
135
+ !props.useStore && setResult(result)
109
136
 
110
- this.$emit('fetch-success', response)
137
+ updateModels({
138
+ errors: viewState.value.errors,
139
+ fields: viewState.value.fields,
140
+ metadata: viewState.value.metadata,
111
141
 
112
- log(`[${this.entity}]:fetchSingle:success`, response)
113
- } catch (error) {
114
- this.mx_fetchError(error)
115
- this.$emit('fetch-error', error)
142
+ ...(!props.useStore && { result: viewState.value.result })
143
+ })
116
144
 
117
- log(`[${this.entity}]:fetchSingle:error`, error)
118
- } finally {
119
- this.mx_isFetching = false
120
- }
121
- }
145
+ emit('fetch-success', response)
146
+
147
+ log(`[${props.entity}]:fetchSingle:success`, response)
148
+ } catch (error) {
149
+ errorHandler(error)
150
+ emit('fetch-error', error)
151
+
152
+ log(`[${props.entity}]:fetchSingle:error`, error)
153
+ } finally {
154
+ viewState.value.fetching = false
122
155
  }
123
156
  }
157
+
158
+ function getAction (payload) {
159
+ if (props.useStore) {
160
+ return qas.getAction({
161
+ entity: props.entity,
162
+ key: 'fetchSingle',
163
+ payload
164
+ })
165
+ }
166
+
167
+ const { id: unusedID, url: unusedURL, ...externalPayload } = payload
168
+
169
+ const decamelizedEntity = decamelize(props.entity, { separator: '-' })
170
+
171
+ const url = props.url || `${decamelizedEntity}/${id.value}`
172
+
173
+ return axios.get(url, { ...externalPayload })
174
+ }
124
175
  </script>
@@ -62,10 +62,18 @@ props:
62
62
  default: true
63
63
  type: Boolean
64
64
 
65
+ use-route:
66
+ desc: Controla se o componente usará internamente uma store do vuex/pinia ou se vai ser utilizado o axios diretamente.
67
+ default: true
68
+ type: Boolean
69
+
65
70
  slots:
66
71
  default:
67
72
  desc: 'Slot para ter o conteúdo principal (dentro do main).'
68
73
 
74
+ fetch-error:
75
+ desc: Slot disponibilizado em caso de fetchError.
76
+
69
77
  footer:
70
78
  desc: 'Slot para acessar o footer.'
71
79
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <qas-box class="q-px-lg q-py-md">
2
+ <component :is="parentComponent.is" v-bind="parentComponent.props">
3
3
  <q-table ref="table" class="bg-white qas-table-generator text-grey-8" v-bind="attributes">
4
4
  <template v-for="(_, name) in slots" #[name]="context">
5
5
  <slot :name="name" v-bind="context" />
@@ -15,7 +15,7 @@
15
15
  </q-td>
16
16
  </template>
17
17
  </q-table>
18
- </qas-box>
18
+ </component>
19
19
  </template>
20
20
 
21
21
  <script>
@@ -57,6 +57,11 @@ export default {
57
57
  type: String
58
58
  },
59
59
 
60
+ useBox: {
61
+ type: Boolean,
62
+ default: true
63
+ },
64
+
60
65
  useScrollOnGrab: {
61
66
  type: Boolean,
62
67
  default: true
@@ -217,6 +222,15 @@ export default {
217
222
 
218
223
  hasScrollOnGrab () {
219
224
  return !!Object.keys(this.scrollOnGrab).length
225
+ },
226
+
227
+ parentComponent () {
228
+ return {
229
+ is: this.useBox ? 'qas-box' : 'div',
230
+ props: {
231
+ class: this.useBox ? 'q-px-lg q-py-md' : ''
232
+ }
233
+ }
220
234
  }
221
235
  },
222
236
 
@@ -36,6 +36,11 @@ props:
36
36
  type: Function
37
37
  examples: ["(row) => ({ path: 'table-generator', params: { id: row.uuid } })"]
38
38
 
39
+ use-box:
40
+ desc: Controla se o componente vai usar QasBox ou div.
41
+ default: true
42
+ type: Boolean
43
+
39
44
  use-external-link:
40
45
  desc: Usado em conjunto com a prop "row-route-fn" quando há a necessidade da rota ser um link externo.
41
46
  default: false
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <div class="qas-toggle-visibility">
3
+ <div class="items-center no-wrap row" :style>
4
+ <div class="ellipsis qas-toggle-visibility__content">
5
+ <div
6
+ v-if="isVisible"
7
+ class="ellipsis full-width"
8
+ :title="props.text"
9
+ >
10
+ <slot>
11
+ {{ props.text }}
12
+ </slot>
13
+ </div>
14
+
15
+ <q-separator
16
+ v-else
17
+ size="4px"
18
+ />
19
+ </div>
20
+
21
+ <qas-btn
22
+ class="q-ml-sm qas-toggle-visibility__button"
23
+ :icon
24
+ @click.prevent.stop="toggleVisibility"
25
+ />
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ import { useToggleVisibility } from '../../composables/private'
32
+
33
+ import { uid } from 'quasar'
34
+ import { computed } from 'vue'
35
+
36
+ defineOptions({ name: 'QasToggleVisibility' })
37
+
38
+ const props = defineProps({
39
+ group: {
40
+ type: String,
41
+ default: ''
42
+ },
43
+
44
+ text: {
45
+ type: String,
46
+ default: ''
47
+ },
48
+
49
+ uuid: {
50
+ type: String,
51
+ default: ''
52
+ },
53
+
54
+ width: {
55
+ type: String,
56
+ default: '140px'
57
+ }
58
+ })
59
+
60
+ const {
61
+ isVisible,
62
+ toggleVisibility
63
+ } = useToggleVisibility({ group: props.group, uuid: props.uuid || uid() })
64
+
65
+ const icon = computed(() => isVisible.value ? 'sym_r_visibility' : 'sym_r_visibility_off')
66
+ const style = computed(() => ({ width: props.width }))
67
+ </script>
68
+
69
+ <style lang="scss">
70
+ .qas-toggle-visibility {
71
+ &__content {
72
+ flex-grow: 1;
73
+ }
74
+
75
+ &__button {
76
+ flex-grow: 0;
77
+ }
78
+ }
79
+ </style>
@@ -0,0 +1,30 @@
1
+ type: component
2
+
3
+ meta:
4
+ desc: Componente para ocultar e exibir conteúdo, utilizado para dados sensíveis.
5
+
6
+ props:
7
+ group:
8
+ desc: Propriedade para criar grupos de separação.
9
+ default: ''
10
+ type: String
11
+ examples: ["name", "document", "users"]
12
+
13
+ text:
14
+ desc: Texto a ser oculto/exibido.
15
+ default: ''
16
+ type: String
17
+
18
+ uuid:
19
+ desc: É criado um uuid random para cada componente, para que possa ser feito o controle interno, porém é possível sobrescrever por esta propriedade.
20
+ default: ''
21
+ type: String
22
+
23
+ width:
24
+ desc: Tamanho do conteúdo a ser oculto/exibido.
25
+ default: '140px'
26
+ type: String
27
+
28
+ slots:
29
+ default:
30
+ desc: Slot para inserir conteúdo a ser oculto/exibido.
@@ -0,0 +1,3 @@
1
+ export { default as useView } from './use-view'
2
+ export { default as useGenerator } from './use-generator'
3
+ export { default as useToggleVisibility } from './use-toggle-visibility'
@@ -21,7 +21,7 @@ export const baseProps = {
21
21
  },
22
22
 
23
23
  gutter: {
24
- default: Spacing.Lg,
24
+ default: undefined,
25
25
  type: [String, Boolean],
26
26
  validator: gutterValidator
27
27
  }
@@ -46,13 +46,19 @@ export default function ({ props = {} }) {
46
46
  const classes = computed(() => {
47
47
  const classesList = ['row']
48
48
 
49
- if (props.gutter) {
50
- classesList.push(`q-col-gutter-${props.gutter}`)
49
+ if (defaultGutter.value) {
50
+ classesList.push(`q-col-gutter-${defaultGutter.value}`)
51
51
  }
52
52
 
53
53
  return classesList
54
54
  })
55
55
 
56
+ const defaultGutter = computed(() => {
57
+ if (props.gutter) return props.gutter
58
+
59
+ return props.useInline ? Spacing.Md : Spacing.Lg
60
+ })
61
+
56
62
  /**
57
63
  * @function
58
64
  * @param {Object} options
@@ -0,0 +1,48 @@
1
+ import { ref, computed } from 'vue'
2
+
3
+ const visibleState = ref({})
4
+
5
+ /**
6
+ * @param {{
7
+ * group?: string,
8
+ * uuid: string
9
+ * }}
10
+ */
11
+ export default function useToggleVisibility ({ group, uuid }) {
12
+ group = group || 'default'
13
+
14
+ visibleState.value[group] = {
15
+ ...visibleState.value[group],
16
+ [uuid]: false
17
+ }
18
+
19
+ const isVisible = computed(() => visibleState.value[group]?.[uuid] || false)
20
+
21
+ function toggleVisibility () {
22
+ const item = visibleState.value[group]
23
+ const lastVisibleItemKey = Object.keys(item).find(key => !!item[key])
24
+
25
+ /**
26
+ * cenário onde esta visível e clicou no mesmo item
27
+ */
28
+ if (visibleState.value[group][uuid]) {
29
+ visibleState.value[group][uuid] = false
30
+ return
31
+ }
32
+
33
+ /**
34
+ * cenário onde tem um item visível e clicou em outro item
35
+ */
36
+ if (lastVisibleItemKey) {
37
+ visibleState.value[group][lastVisibleItemKey] = false
38
+ }
39
+
40
+ visibleState.value[group][uuid] = true
41
+ }
42
+
43
+ return {
44
+ isVisible,
45
+
46
+ toggleVisibility
47
+ }
48
+ }
@@ -0,0 +1,186 @@
1
+ import { NotifyError } from '../../plugins'
2
+ import { camelizeFieldsName } from '../../helpers'
3
+
4
+ import { useView as useViewComposable } from '@bildvitta/composables'
5
+ import { ref, computed, watch, markRaw } from 'vue'
6
+ import { useRouter } from 'vue-router'
7
+
8
+ export const baseProps = {
9
+ beforeFetch: {
10
+ default: null,
11
+ type: Function
12
+ },
13
+
14
+ errors: {
15
+ default: () => ({}),
16
+ type: Object
17
+ },
18
+
19
+ entity: {
20
+ type: String,
21
+ required: ''
22
+ },
23
+
24
+ fetching: {
25
+ type: Boolean
26
+ },
27
+
28
+ fields: {
29
+ default: () => ({}),
30
+ type: Object
31
+ },
32
+
33
+ metadata: {
34
+ default: () => ({}),
35
+ type: Object
36
+ },
37
+
38
+ url: {
39
+ default: '',
40
+ type: String
41
+ },
42
+
43
+ useBoundary: {
44
+ default: true,
45
+ type: Boolean
46
+ },
47
+
48
+ useStore: {
49
+ default: true,
50
+ type: Boolean
51
+ }
52
+ }
53
+
54
+ export const baseEmits = [
55
+ 'update:fields',
56
+ 'update:errors',
57
+ 'update:metadata',
58
+ 'update:fetching'
59
+ ]
60
+
61
+ /**
62
+ * @param {{
63
+ * mode: import('@bildvitta/composables').ViewModeTypes,
64
+ * props: baseProps,
65
+ * emit: (baseEmits: string) => void,
66
+ * slots: import('vue').Slots
67
+ * }} config
68
+ */
69
+ export default function useView (config) {
70
+ const {
71
+ emit,
72
+ mode,
73
+ slots,
74
+ props
75
+ } = config
76
+
77
+ // composables
78
+ const router = useRouter()
79
+ const { viewState } = useViewComposable({ mode })
80
+
81
+ // refs
82
+ const cancelBeforeFetch = ref(false)
83
+ const hasFetchError = ref(false)
84
+
85
+ // constants
86
+ const fetchErrorMessage = 'Ops… Não conseguimos acessar as informações. Por favor, tente novamente em alguns minutos.'
87
+
88
+ // computed
89
+ const componentClass = computed(() => props.useBoundary && 'container spaced')
90
+ const hasFooterSlot = computed(() => !!slots.footer)
91
+ const hasHeaderSlot = computed(() => !!slots.header)
92
+ const hasFetchErrorSlot = computed(() => !!slots['fetch-error'])
93
+ const canShowFetchErrorSlot = computed(() => hasFetchErrorSlot.value && hasFetchError.value)
94
+
95
+ // watch
96
+ watch(() => viewState.value.fetching, value => emit('update:fetching', value))
97
+
98
+ // functions
99
+ function errorHandler (error) {
100
+ const { response } = error
101
+
102
+ const status = response?.status
103
+
104
+ const redirect = status >= 500
105
+ ? 'ServerError'
106
+ : ({ 403: 'Forbidden', 404: 'NotFound' })[status]
107
+
108
+ /**
109
+ * caso exista um desses status será redirecionado sem aparecer o "notify"
110
+ */
111
+ if (redirect) {
112
+ router.replace({ name: redirect })
113
+
114
+ return
115
+ }
116
+
117
+ hasFetchError.value = true
118
+
119
+ NotifyError(fetchErrorMessage)
120
+ }
121
+
122
+ function setErrors (errors = {}) {
123
+ viewState.value.errors = markRaw(errors)
124
+ }
125
+
126
+ function setFields (fields = {}) {
127
+ const camelizedFields = camelizeFieldsName(fields)
128
+
129
+ viewState.value.fields = markRaw(camelizedFields)
130
+ }
131
+
132
+ function setMetadata (metadata = {}) {
133
+ viewState.value.metadata = markRaw(metadata)
134
+ }
135
+
136
+ function setResult (result = {}) {
137
+ viewState.value.result = result
138
+ }
139
+
140
+ function updateModels (models) {
141
+ for (const key in models) {
142
+ if (!models[key]) continue
143
+
144
+ emit(`update:${key}`, models[key])
145
+ }
146
+ }
147
+
148
+ function fetchHandler (payload, resolve) {
149
+ const hasBeforeFetch = typeof props.beforeFetch === 'function'
150
+
151
+ if (hasBeforeFetch && !cancelBeforeFetch.value) {
152
+ return props.beforeFetch({
153
+ payload,
154
+ resolve: payload => resolve(payload),
155
+ done: () => {
156
+ cancelBeforeFetch.value = true
157
+ }
158
+ })
159
+ }
160
+
161
+ resolve()
162
+ }
163
+
164
+ return {
165
+ // state
166
+ viewState,
167
+
168
+ // constants
169
+ fetchErrorMessage,
170
+
171
+ // computed
172
+ componentClass,
173
+ hasFooterSlot,
174
+ hasHeaderSlot,
175
+ canShowFetchErrorSlot,
176
+
177
+ // functions
178
+ errorHandler,
179
+ fetchHandler,
180
+ setErrors,
181
+ setFields,
182
+ setResult,
183
+ setMetadata,
184
+ updateModels
185
+ }
186
+ }
@@ -56,7 +56,8 @@ export default {
56
56
  mx_fields: {},
57
57
  mx_metadata: {},
58
58
  mx_cancelBeforeFetch: false,
59
- mx_isFetching: false
59
+ mx_isFetching: false,
60
+ mx_hasFetchError: false
60
61
  }
61
62
  },
62
63
 
@@ -79,8 +80,16 @@ export default {
79
80
  return !!(this.$slots.header)
80
81
  },
81
82
 
83
+ mx_hasFetchErrorSlot () {
84
+ return !!(this.$slots['fetch-error'])
85
+ },
86
+
82
87
  mx_fetchErrorMessage () {
83
88
  return 'Ops… Não conseguimos acessar as informações. Por favor, tente novamente em alguns minutos.'
89
+ },
90
+
91
+ mx_canShowFetchErrorSlot () {
92
+ return this.mx_hasFetchError && this.mx_hasFetchErrorSlot
84
93
  }
85
94
  },
86
95
 
@@ -101,6 +110,8 @@ export default {
101
110
  return
102
111
  }
103
112
 
113
+ this.mx_hasFetchError = true
114
+
104
115
  this.$qas.error(this.mx_fetchErrorMessage)
105
116
  },
106
117
 
package/src/vue-plugin.js CHANGED
@@ -64,6 +64,7 @@ import QasTableGenerator from './components/table-generator/QasTableGenerator.vu
64
64
  import QasTabsGenerator from './components/tabs-generator/QasTabsGenerator.vue'
65
65
  import QasTextTruncate from './components/text-truncate/QasTextTruncate.vue'
66
66
  import QasTimeline from './components/timeline/QasTimeline.vue'
67
+ import QasToggleVisibility from './components/toggle-visibility/QasToggleVisibility.vue'
67
68
  import QasTransfer from './components/transfer/QasTransfer.vue'
68
69
  import QasTreeGenerator from './components/tree-generator/QasTreeGenerator.vue'
69
70
  import QasUploader from './components/uploader/QasUploader.vue'
@@ -72,7 +73,7 @@ import QasWhatsappLink from './components/whatsapp-link/QasWhatsappLink.vue'
72
73
 
73
74
  import { Notify, Loading, Quasar, Dialog as QuasarDialog } from 'quasar'
74
75
 
75
- import { getAction } from '@bildvitta/store-adapter'
76
+ import { getAction, getGetter } from '@bildvitta/store-adapter'
76
77
 
77
78
  // Plugins
78
79
  import {
@@ -157,6 +158,7 @@ async function install (app) {
157
158
  app.component('QasTabsGenerator', QasTabsGenerator)
158
159
  app.component('QasTextTruncate', QasTextTruncate)
159
160
  app.component('QasTimeline', QasTimeline)
161
+ app.component('QasToggleVisibility', QasToggleVisibility)
160
162
  app.component('QasTransfer', QasTransfer)
161
163
  app.component('QasTreeGenerator', QasTreeGenerator)
162
164
  app.component('QasUploader', QasUploader)
@@ -176,7 +178,8 @@ async function install (app) {
176
178
  app.provide('qas', {
177
179
  delete: params => Delete.call(app.config.globalProperties, params),
178
180
 
179
- getAction: params => getAction.call(app.config.globalProperties, params)
181
+ getAction: params => getAction.call(app.config.globalProperties, params),
182
+ getGetter: params => getGetter.call(app.config.globalProperties, params)
180
183
  })
181
184
 
182
185
  app.directive(Test.name, Test)
@@ -251,6 +254,7 @@ export {
251
254
  QasTabsGenerator,
252
255
  QasTextTruncate,
253
256
  QasTimeline,
257
+ QasToggleVisibility,
254
258
  QasTransfer,
255
259
  QasTreeGenerator,
256
260
  QasUploader,