@bildvitta/quasar-ui-asteroid 3.14.0-beta.2 → 3.14.0-beta.3
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 +1 -1
- package/src/components/app-bar/QasAppBar.vue +6 -1
- package/src/components/app-bar/QasAppBar.yml +3 -0
- package/src/components/app-menu/QasAppMenu.vue +3 -2
- package/src/components/app-menu/QasAppMenu.yml +3 -0
- package/src/components/app-menu/composables/use-app-user.js +3 -0
- package/src/components/app-user/QasAppUser.vue +68 -13
- package/src/components/app-user/QasAppUser.yml +3 -0
- package/src/components/avatar/QasAvatar.vue +9 -3
- package/src/components/avatar/QasAvatar.yml +1 -1
- package/src/components/avatar/enums/AvatarColors.js +2 -1
- package/src/components/card/QasCard.vue +3 -3
- package/src/components/dialog/QasDialog.vue +29 -1
- package/src/components/drawer/QasDrawer.vue +117 -0
- package/src/components/drawer/QasDrawer.yml +57 -0
- package/src/components/infinite-scroll/QasInfiniteScroll.vue +6 -2
- package/src/components/infinite-scroll/QasInfiniteScroll.yml +22 -0
- package/src/components/layout/QasLayout.vue +83 -52
- package/src/components/layout/QasLayout.yml +5 -0
- package/src/components/layout/private/PvLayoutNotificationCard.vue +86 -0
- package/src/components/layout/private/PvLayoutNotificationsDrawer.vue +141 -0
- package/src/components/list-items/QasListItems.vue +33 -5
- package/src/components/list-items/QasListItems.yml +5 -0
- package/src/composables/index.js +3 -0
- package/src/composables/use-notifications.js +114 -0
- package/src/css/plugins/notify.scss +40 -2
- package/src/helpers/private/has-parent-by-class-name.js +15 -0
- package/src/vue-plugin.js +3 -0
package/package.json
CHANGED
|
@@ -54,7 +54,7 @@ const props = defineProps({
|
|
|
54
54
|
}
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
const emit = defineEmits(['sign-out', 'toggle-menu'])
|
|
57
|
+
const emit = defineEmits(['sign-out', 'toggle-menu', 'toggle-notifications'])
|
|
58
58
|
|
|
59
59
|
const router = useRouter()
|
|
60
60
|
const screen = useScreen()
|
|
@@ -68,6 +68,7 @@ const defaultAppUserProps = computed(() => {
|
|
|
68
68
|
},
|
|
69
69
|
|
|
70
70
|
onSignOut: signOut,
|
|
71
|
+
onToggleNotifications: toggleNotifications,
|
|
71
72
|
...props.appUserProps
|
|
72
73
|
}
|
|
73
74
|
})
|
|
@@ -96,6 +97,10 @@ function signOut () {
|
|
|
96
97
|
emit('sign-out')
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
function toggleNotifications () {
|
|
101
|
+
emit('toggle-notifications')
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
function toggleMenuDrawer () {
|
|
100
105
|
emit('toggle-menu')
|
|
101
106
|
}
|
|
@@ -146,7 +146,7 @@ const props = defineProps({
|
|
|
146
146
|
}
|
|
147
147
|
})
|
|
148
148
|
|
|
149
|
-
const emit = defineEmits(['sign-out', 'update:modelValue'])
|
|
149
|
+
const emit = defineEmits(['sign-out', 'update:modelValue', 'toggle-notifications'])
|
|
150
150
|
|
|
151
151
|
const screen = useScreen()
|
|
152
152
|
const router = useRouter()
|
|
@@ -159,7 +159,8 @@ const isMini = ref(screen.isLarge)
|
|
|
159
159
|
const composableParams = {
|
|
160
160
|
props,
|
|
161
161
|
onMenuUpdate: setHasOpenedMenu,
|
|
162
|
-
onSignOut: () => emit('sign-out')
|
|
162
|
+
onSignOut: () => emit('sign-out'),
|
|
163
|
+
onToggleNotifications: () => emit('toggle-notifications')
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
const { defaultAppUserProps, showAppUser } = useAppUser(composableParams)
|
|
@@ -57,6 +57,9 @@ events:
|
|
|
57
57
|
'@sign-out -> function()':
|
|
58
58
|
desc: Dispara quando o botão de "sair" é clicado.
|
|
59
59
|
|
|
60
|
+
'@toggle-notifications -> function()':
|
|
61
|
+
desc: Dispara quando o botão de "Notificações" é clicado.
|
|
62
|
+
|
|
60
63
|
'@update:model-value -> function(value)':
|
|
61
64
|
desc: Dispara quando o model-value altera, também usado para v-model.
|
|
62
65
|
params:
|
|
@@ -6,6 +6,7 @@ import { computed } from 'vue'
|
|
|
6
6
|
* @param {{
|
|
7
7
|
* props: { appUserProps: {} }
|
|
8
8
|
* onSignOut: () => void
|
|
9
|
+
* onToggleNotifications: () => void
|
|
9
10
|
* onMenuUpdate: () => void
|
|
10
11
|
* }} config
|
|
11
12
|
*/
|
|
@@ -14,6 +15,7 @@ export default function useAppUser (config = {}) {
|
|
|
14
15
|
props,
|
|
15
16
|
|
|
16
17
|
onSignOut,
|
|
18
|
+
onToggleNotifications,
|
|
17
19
|
onMenuUpdate
|
|
18
20
|
} = config
|
|
19
21
|
|
|
@@ -29,6 +31,7 @@ export default function useAppUser (config = {}) {
|
|
|
29
31
|
|
|
30
32
|
// eventos
|
|
31
33
|
onSignOut,
|
|
34
|
+
onToggleNotifications,
|
|
32
35
|
...props.appUserProps
|
|
33
36
|
}
|
|
34
37
|
})
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
<div class="relative-position">
|
|
4
4
|
<qas-avatar :image="props.user.photo" :size="props.avatarSize" :title="userName" />
|
|
5
5
|
|
|
6
|
-
<
|
|
7
|
-
{{ props.notifications.count }}
|
|
8
|
-
</q-badge>
|
|
6
|
+
<qas-avatar v-if="hasNotificationInUserAvatar" v-bind="avatarNotificationCountProps" />
|
|
9
7
|
</div>
|
|
10
8
|
|
|
11
9
|
<div class="ellipsis qas-app-user__data">
|
|
@@ -43,20 +41,16 @@
|
|
|
43
41
|
</q-item-section>
|
|
44
42
|
</q-item>
|
|
45
43
|
|
|
46
|
-
<q-item v-if="
|
|
47
|
-
<q-item-section avatar>
|
|
44
|
+
<q-item v-if="isNotificationsEnabled" v-close-popup class="qas-app-user__menu-item" clickable @click="toggleNotificationsDrawer">
|
|
45
|
+
<q-item-section avatar class="relative-position">
|
|
48
46
|
<q-icon name="sym_r_notifications" />
|
|
47
|
+
|
|
48
|
+
<qas-avatar v-if="hasUnreadNotifications" class="qas-app-user__notification-avatar--icon" v-bind="avatarNotificationCountProps" />
|
|
49
49
|
</q-item-section>
|
|
50
50
|
|
|
51
51
|
<q-item-section>
|
|
52
52
|
Notificações
|
|
53
53
|
</q-item-section>
|
|
54
|
-
|
|
55
|
-
<q-item-section side>
|
|
56
|
-
<q-badge color="red">
|
|
57
|
-
{{ props.notifications.count }}
|
|
58
|
-
</q-badge>
|
|
59
|
-
</q-item-section>
|
|
60
54
|
</q-item>
|
|
61
55
|
|
|
62
56
|
<q-item v-close-popup class="qas-app-user__menu-item" clickable @click="signOut">
|
|
@@ -77,6 +71,7 @@
|
|
|
77
71
|
<script setup>
|
|
78
72
|
import QasAvatar from '../avatar/QasAvatar.vue'
|
|
79
73
|
|
|
74
|
+
import useNotifications from '../../composables/use-notifications'
|
|
80
75
|
import { NotifySuccess, NotifyError } from '../../plugins'
|
|
81
76
|
|
|
82
77
|
import { ref, computed, watch, inject } from 'vue'
|
|
@@ -111,14 +106,19 @@ const props = defineProps({
|
|
|
111
106
|
}
|
|
112
107
|
})
|
|
113
108
|
|
|
114
|
-
const emit = defineEmits(['sign-out'])
|
|
109
|
+
const emit = defineEmits(['sign-out', 'toggle-notifications'])
|
|
115
110
|
|
|
116
111
|
// vindo direto do boot api.js
|
|
117
112
|
const axios = inject('axios')
|
|
118
113
|
|
|
114
|
+
const { isNotificationsEnabled, unreadNotificationsCount } = useNotifications()
|
|
115
|
+
|
|
119
116
|
const companiesModel = ref('')
|
|
120
117
|
const loading = ref(false)
|
|
121
118
|
|
|
119
|
+
const { avatarNotificationCountProps } = useAvatarNotifications()
|
|
120
|
+
|
|
121
|
+
// computed
|
|
122
122
|
const defaultCompanyProps = computed(() => {
|
|
123
123
|
return {
|
|
124
124
|
loading: loading.value,
|
|
@@ -131,8 +131,10 @@ const defaultCompanyProps = computed(() => {
|
|
|
131
131
|
})
|
|
132
132
|
|
|
133
133
|
const hasCompaniesSelect = computed(() => !!props.companyProps.options?.length)
|
|
134
|
-
const
|
|
134
|
+
const hasUnreadNotifications = computed(() => unreadNotificationsCount.value > 0)
|
|
135
|
+
const hasNotificationInUserAvatar = computed(() => isNotificationsEnabled && hasUnreadNotifications.value)
|
|
135
136
|
|
|
137
|
+
const unreadNotificationsToString = computed(() => String(unreadNotificationsCount.value))
|
|
136
138
|
const userName = computed(() => props.user.name || props.user.givenName)
|
|
137
139
|
|
|
138
140
|
// watch
|
|
@@ -140,6 +142,41 @@ watch(() => props.companyProps.modelValue, value => {
|
|
|
140
142
|
companiesModel.value = value
|
|
141
143
|
}, { immediate: true })
|
|
142
144
|
|
|
145
|
+
// composable
|
|
146
|
+
function useAvatarNotifications () {
|
|
147
|
+
const hasAnimated = ref(false)
|
|
148
|
+
|
|
149
|
+
watch(() => unreadNotificationsCount.value, () => {
|
|
150
|
+
hasAnimated.value = true
|
|
151
|
+
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
hasAnimated.value = false
|
|
154
|
+
}, 1000)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const avatarNotificationCountProps = computed(() => {
|
|
158
|
+
const classes = [
|
|
159
|
+
'qas-app-user__notification-avatar',
|
|
160
|
+
'animated',
|
|
161
|
+
{
|
|
162
|
+
rubberBand: hasAnimated.value
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
class: classes,
|
|
168
|
+
color: 'red-14',
|
|
169
|
+
size: 'xs',
|
|
170
|
+
title: unreadNotificationsToString.value,
|
|
171
|
+
useCropTitle: false
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
avatarNotificationCountProps
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
143
180
|
// métodos
|
|
144
181
|
function signOut () {
|
|
145
182
|
emit('sign-out')
|
|
@@ -169,10 +206,28 @@ function onMenuHide () {
|
|
|
169
206
|
companiesModel.value = props.companyProps.modelValue
|
|
170
207
|
}
|
|
171
208
|
}
|
|
209
|
+
|
|
210
|
+
function toggleNotificationsDrawer () {
|
|
211
|
+
emit('toggle-notifications')
|
|
212
|
+
}
|
|
172
213
|
</script>
|
|
173
214
|
|
|
174
215
|
<style lang="scss">
|
|
175
216
|
.qas-app-user {
|
|
217
|
+
&__notification-avatar {
|
|
218
|
+
animation-duration: 1s;
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: 0;
|
|
221
|
+
|
|
222
|
+
&:not(&--icon) {
|
|
223
|
+
right: -4px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
&--icon {
|
|
227
|
+
right: 4px;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
176
231
|
&__data {
|
|
177
232
|
line-height: 1.1;
|
|
178
233
|
}
|
|
@@ -43,6 +43,9 @@ events:
|
|
|
43
43
|
'@sign-out -> function()':
|
|
44
44
|
desc: Dispara quando o botão de "sair" é clicado.
|
|
45
45
|
|
|
46
|
+
'@toggle-notifications -> function()':
|
|
47
|
+
desc: Dispara quando o botão de "Notificações" é clicado.
|
|
48
|
+
|
|
46
49
|
selectors:
|
|
47
50
|
app-user:
|
|
48
51
|
desc: Seletor do componente.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<q-avatar class="text-bold" v-bind="attributes">
|
|
3
3
|
<q-img v-if="hasImage" :alt="props.title" :ratio="1" spinner-color="primary" spinner-size="16px" :src="props.image" @error="onImageLoadedError" />
|
|
4
|
-
<template v-else-if="hasTitle">{{
|
|
4
|
+
<template v-else-if="hasTitle">{{ label }}</template>
|
|
5
5
|
<q-icon v-else :name="props.icon" />
|
|
6
6
|
</q-avatar>
|
|
7
7
|
</template>
|
|
@@ -45,6 +45,11 @@ const props = defineProps({
|
|
|
45
45
|
title: {
|
|
46
46
|
default: '',
|
|
47
47
|
type: String
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
useCropTitle: {
|
|
51
|
+
type: Boolean,
|
|
52
|
+
default: true
|
|
48
53
|
}
|
|
49
54
|
})
|
|
50
55
|
|
|
@@ -64,7 +69,8 @@ const attributes = computed(() => {
|
|
|
64
69
|
const colors = {
|
|
65
70
|
[AvatarColors.Primary]: 'white',
|
|
66
71
|
[AvatarColors.SecondaryContrast]: 'primary',
|
|
67
|
-
[AvatarColors.Grey4]: 'grey-8'
|
|
72
|
+
[AvatarColors.Grey4]: 'grey-8',
|
|
73
|
+
[AvatarColors.Red14]: 'white'
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
return {
|
|
@@ -75,7 +81,7 @@ const attributes = computed(() => {
|
|
|
75
81
|
}
|
|
76
82
|
})
|
|
77
83
|
|
|
78
|
-
const
|
|
84
|
+
const label = computed(() => props.useCropTitle ? props.title[0]?.toUpperCase?.() : props.title)
|
|
79
85
|
|
|
80
86
|
const hasImage = computed(() => !hasImageError.value && !!props.image)
|
|
81
87
|
const hasTitle = computed(() => !!props.title)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="col-12 col-lg-3 col-md-4 col-sm-6">
|
|
3
3
|
<q-card class="border-radius-lg column full-height overflow-hidden" :class="cardClasses">
|
|
4
|
-
<header v-if="props.useHeader" class="overflow-hidden relative-position
|
|
4
|
+
<header v-if="props.useHeader" class="full-width overflow-hidden relative-position">
|
|
5
5
|
<slot name="header">
|
|
6
6
|
<q-carousel v-model="slideImage" animated class="cursor-pointer" height="205px" infinite :navigation="hasImages" navigation-icon="sym_r_fiber_manual_record" swipeable>
|
|
7
7
|
<template #navigation-icon="{ active, btnProps, onClick }">
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
</slot>
|
|
18
18
|
</header>
|
|
19
19
|
|
|
20
|
-
<q-card-section class="col-grow column justify-between
|
|
21
|
-
<div class="
|
|
20
|
+
<q-card-section class="col-grow column full-width justify-between">
|
|
21
|
+
<div class="full-width" :class="gutterClass">
|
|
22
22
|
<slot />
|
|
23
23
|
</div>
|
|
24
24
|
</q-card-section>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<q-dialog ref="dialogRef" class="qas-dialog" data-cy="dialog" :persistent="props.persistent"
|
|
2
|
+
<q-dialog ref="dialogRef" class="qas-dialog" :class="classes" data-cy="dialog" v-bind="dialogProps" :persistent="props.persistent" @update:model-value="updateModelValue">
|
|
3
3
|
<div class="bg-white q-pa-lg" :style="style">
|
|
4
4
|
<header v-if="hasHeader" class="q-mb-lg">
|
|
5
5
|
<slot name="header">
|
|
@@ -136,6 +136,21 @@ const { defaultCancel, hasCancel } = useCancel(composablesParams)
|
|
|
136
136
|
const { defaultOk, hasOk, onOk } = useOk(composablesParams)
|
|
137
137
|
const { descriptionComponent, mainComponent } = useDynamicComponents({ ...composablesParams, onOk, hasOk })
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Classes criadas para serem utilizadas quando usado com a prop "position", pois
|
|
141
|
+
* o comportamento do dialog muda, e não é possível usar em conjunto com a prop
|
|
142
|
+
* "useFullMaxWidth", então foi necessário uma trativa.
|
|
143
|
+
*/
|
|
144
|
+
const classes = computed(() => {
|
|
145
|
+
const isRightPosition = attrs.position === 'right'
|
|
146
|
+
const isLeftPosition = attrs.position === 'left'
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
'qas-dialog--right': isRightPosition,
|
|
150
|
+
'qas-dialog--left': isLeftPosition
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
139
154
|
const dialogProps = computed(() => {
|
|
140
155
|
return {
|
|
141
156
|
...(!props.usePlugin && { modelValue: props.modelValue }),
|
|
@@ -183,5 +198,18 @@ function updateModelValue (value) {
|
|
|
183
198
|
.q-dialog__inner > div {
|
|
184
199
|
box-shadow: $shadow-2;
|
|
185
200
|
}
|
|
201
|
+
|
|
202
|
+
&--right {
|
|
203
|
+
.q-dialog__inner {
|
|
204
|
+
width: 100%;
|
|
205
|
+
justify-content: end;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
&--left {
|
|
210
|
+
.q-dialog__inner {
|
|
211
|
+
width: 100%;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
186
214
|
}
|
|
187
215
|
</style>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<qas-dialog class="qas-drawer" v-bind="attributes" @update:model-value="onUpdateModelValue">
|
|
3
|
+
<template #header>
|
|
4
|
+
<div class="items-center justify-between row">
|
|
5
|
+
<span data-cy="drawer-title">
|
|
6
|
+
<slot name="title">
|
|
7
|
+
<h3 v-if="props.title" class="text-h3">
|
|
8
|
+
{{ props.title }}
|
|
9
|
+
</h3>
|
|
10
|
+
</slot>
|
|
11
|
+
</span>
|
|
12
|
+
|
|
13
|
+
<qas-btn v-close-popup class="z-max" color="grey-10" data-cy="drawer-close-btn" icon="sym_r_close" variant="tertiary" @click="emit('update:modelValue', false)" />
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<template #description>
|
|
18
|
+
<div>
|
|
19
|
+
<div class="relative-position" data-cy="drawer-default">
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div v-if="props.loading" class="qas-drawer__loading" :style="loadingStyle">
|
|
24
|
+
<div class="full-height relative-position">
|
|
25
|
+
<q-inner-loading :showing="props.loading">
|
|
26
|
+
<q-spinner color="grey" size="2em" />
|
|
27
|
+
</q-inner-loading>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
</qas-dialog>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import useScreen from '../../composables/use-screen.js'
|
|
37
|
+
|
|
38
|
+
import { computed, useAttrs } from 'vue'
|
|
39
|
+
|
|
40
|
+
defineOptions({
|
|
41
|
+
name: 'QasDrawer',
|
|
42
|
+
inheritAttrs: false
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const props = defineProps({
|
|
46
|
+
dialogProps: {
|
|
47
|
+
type: Object,
|
|
48
|
+
default: () => ({})
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
maxWidth: {
|
|
52
|
+
type: String,
|
|
53
|
+
default: ''
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
position: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: 'left',
|
|
59
|
+
validator: value => ['left', 'right'].includes(value)
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
title: {
|
|
63
|
+
type: String,
|
|
64
|
+
default: ''
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
loading: {
|
|
68
|
+
type: Boolean
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const emit = defineEmits(['update:modelValue'])
|
|
73
|
+
|
|
74
|
+
const attrs = useAttrs()
|
|
75
|
+
const screen = useScreen()
|
|
76
|
+
|
|
77
|
+
const loadingStyle = computed(() => {
|
|
78
|
+
return {
|
|
79
|
+
right: `calc(100% - ${props.maxWidth})`
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const attributes = computed(() => {
|
|
84
|
+
const maxWidth = screen.isSmall ? '100%' : props.maxWidth
|
|
85
|
+
|
|
86
|
+
const { modelValue } = attrs
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
persistent: false,
|
|
90
|
+
modelValue,
|
|
91
|
+
|
|
92
|
+
...props.dialogProps,
|
|
93
|
+
|
|
94
|
+
cancel: false,
|
|
95
|
+
maxWidth,
|
|
96
|
+
maximized: true,
|
|
97
|
+
ok: false,
|
|
98
|
+
position: props.position,
|
|
99
|
+
useFullMaxWidth: true
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
function onUpdateModelValue (value) {
|
|
104
|
+
emit('update:modelValue', value)
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<style lang="scss">
|
|
109
|
+
.qas-drawer {
|
|
110
|
+
&__loading {
|
|
111
|
+
height: 100vh;
|
|
112
|
+
left: 0;
|
|
113
|
+
position: absolute;
|
|
114
|
+
top: 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type: component
|
|
2
|
+
|
|
3
|
+
meta:
|
|
4
|
+
desc: Componente de drawer que implementa o QasDialog.
|
|
5
|
+
|
|
6
|
+
props:
|
|
7
|
+
dialog-props:
|
|
8
|
+
desc: Repassa propriedades para o QasDialog.
|
|
9
|
+
default: {}
|
|
10
|
+
type: Object
|
|
11
|
+
|
|
12
|
+
max-width:
|
|
13
|
+
desc: Tamanho máximo do dialog.
|
|
14
|
+
type: String
|
|
15
|
+
|
|
16
|
+
model-value:
|
|
17
|
+
desc: Model do componente, abre ou fecha o drawer.
|
|
18
|
+
type: Boolean
|
|
19
|
+
examples: [v-model="value"]
|
|
20
|
+
model: true
|
|
21
|
+
|
|
22
|
+
position:
|
|
23
|
+
desc: 'Posição do drawer, sendo possível apenas 2 opções: [left, right].'
|
|
24
|
+
default: left
|
|
25
|
+
type: String
|
|
26
|
+
|
|
27
|
+
title:
|
|
28
|
+
desc: Titulo do drawer.
|
|
29
|
+
type: String
|
|
30
|
+
|
|
31
|
+
slots:
|
|
32
|
+
default:
|
|
33
|
+
desc: Slot para conteúdo principal.
|
|
34
|
+
|
|
35
|
+
title:
|
|
36
|
+
desc: Slot para o titulo.
|
|
37
|
+
|
|
38
|
+
events:
|
|
39
|
+
'@update:model-value -> function (value)':
|
|
40
|
+
desc: Dispara toda vez que o model é atualizado, também utilizado para v-model.
|
|
41
|
+
params:
|
|
42
|
+
value:
|
|
43
|
+
desc: Novo valor do v-model
|
|
44
|
+
type: Boolean
|
|
45
|
+
|
|
46
|
+
selectors:
|
|
47
|
+
drawer-close-btn:
|
|
48
|
+
desc: Seletor do botão de fechar.
|
|
49
|
+
examples: ['data-cy="drawer-close-btn"']
|
|
50
|
+
|
|
51
|
+
drawer-default:
|
|
52
|
+
desc: Seletor do botão conteúdo do slot default.
|
|
53
|
+
examples: ['data-cy="drawer-default"']
|
|
54
|
+
|
|
55
|
+
drawer-title:
|
|
56
|
+
desc: Seletor do titulo.
|
|
57
|
+
examples: ['data-cy="drawer-title"']
|
|
@@ -62,7 +62,7 @@ const props = defineProps({
|
|
|
62
62
|
|
|
63
63
|
defineExpose({ refresh, remove })
|
|
64
64
|
|
|
65
|
-
const emit = defineEmits(['update:list'])
|
|
65
|
+
const emit = defineEmits(['update:list', 'fetch-success', 'fetch-error'])
|
|
66
66
|
|
|
67
67
|
const axios = inject('axios')
|
|
68
68
|
|
|
@@ -135,10 +135,14 @@ async function fetchList () {
|
|
|
135
135
|
* após buscar uma vez e retornar uma lista vazia.
|
|
136
136
|
*/
|
|
137
137
|
hasMadeFirstFetch.value = true
|
|
138
|
-
|
|
138
|
+
|
|
139
|
+
emit('fetch-success', { list: newList, offset: offset.value, count: count.value })
|
|
140
|
+
} catch (error) {
|
|
139
141
|
NotifyError('Ops… Não conseguimos acessar as informações. Por favor, tente novamente em alguns minutos.')
|
|
140
142
|
|
|
141
143
|
hasFetchingError.value = true
|
|
144
|
+
|
|
145
|
+
emit('fetch-error', error)
|
|
142
146
|
} finally {
|
|
143
147
|
isFetching.value = false
|
|
144
148
|
}
|
|
@@ -44,6 +44,28 @@ events:
|
|
|
44
44
|
desc: Novo valor do list
|
|
45
45
|
type: Array
|
|
46
46
|
|
|
47
|
+
'@fetch-error -> function (error)':
|
|
48
|
+
desc: Dispara toda vez que ocorre algum erro ao fazer nova busca na API.
|
|
49
|
+
params:
|
|
50
|
+
error:
|
|
51
|
+
count: Erro enviado da API.
|
|
52
|
+
type: Object
|
|
53
|
+
|
|
54
|
+
'@fetch-success -> function ({ count, list, offset })':
|
|
55
|
+
desc: Dispara toda vez que é feito uma nova busca com sucesso na API.
|
|
56
|
+
params:
|
|
57
|
+
count:
|
|
58
|
+
count: Tamanho máximo de itens.
|
|
59
|
+
type: Number
|
|
60
|
+
|
|
61
|
+
list:
|
|
62
|
+
desc: Lista do endpoint.
|
|
63
|
+
type: Array
|
|
64
|
+
|
|
65
|
+
offset:
|
|
66
|
+
desc: valor atual que indica a posição inicial ao recuperar resultados da lista.
|
|
67
|
+
type: Number
|
|
68
|
+
|
|
47
69
|
slots:
|
|
48
70
|
default:
|
|
49
71
|
desc: slot para exibir a lista na qual o componente fez a busca.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<q-layout view="hHh Lpr lff">
|
|
3
3
|
<slot v-if="$qas.screen.untilLarge" name="app-bar">
|
|
4
|
-
<qas-app-bar v-bind="appBarProps" @sign-out="signOut" @toggle-menu="toggleMenuDrawer" />
|
|
4
|
+
<qas-app-bar v-bind="appBarProps" @sign-out="signOut" @toggle-menu="toggleMenuDrawer" @toggle-notifications="toggleNotificationsDrawer" />
|
|
5
5
|
</slot>
|
|
6
6
|
|
|
7
7
|
<slot name="app-menu">
|
|
8
|
-
<qas-app-menu :model-value="showMenuDrawer" v-bind="defaultAppMenuProps" @sign-out="signOut" @update:model-value="updateMenuDrawer" />
|
|
8
|
+
<qas-app-menu :model-value="showMenuDrawer" v-bind="defaultAppMenuProps" @sign-out="signOut" @toggle-notifications="toggleNotificationsDrawer" @update:model-value="updateMenuDrawer" />
|
|
9
9
|
</slot>
|
|
10
10
|
|
|
11
11
|
<slot>
|
|
@@ -17,72 +17,103 @@
|
|
|
17
17
|
</slot>
|
|
18
18
|
|
|
19
19
|
<q-ajax-bar color="primary" position="bottom" size="2px" />
|
|
20
|
+
|
|
21
|
+
<pv-layout-notifications-drawer v-if="isNotificationsEnabled" v-model="notificationsDrawer" />
|
|
20
22
|
</q-layout>
|
|
21
23
|
</template>
|
|
22
24
|
|
|
23
|
-
<script>
|
|
25
|
+
<script setup>
|
|
26
|
+
import PvLayoutNotificationsDrawer from './private/PvLayoutNotificationsDrawer.vue'
|
|
24
27
|
import QasAppBar from '../app-bar/QasAppBar.vue'
|
|
25
28
|
import QasAppMenu from '../app-menu/QasAppMenu.vue'
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
import useScreen from '../../composables/use-screen'
|
|
31
|
+
import useNotifications from '../../composables/use-notifications'
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
QasAppBar,
|
|
32
|
-
QasAppMenu
|
|
33
|
-
},
|
|
33
|
+
import { computed, ref, watch } from 'vue'
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
appBarProps: {
|
|
37
|
-
default: () => ({}),
|
|
38
|
-
type: Object
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
appMenuProps: {
|
|
42
|
-
default: () => ({}),
|
|
43
|
-
type: Object
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
modelValue: {
|
|
47
|
-
default: true,
|
|
48
|
-
type: Boolean
|
|
49
|
-
}
|
|
50
|
-
},
|
|
35
|
+
defineOptions({ name: 'QasLayout' })
|
|
51
36
|
|
|
52
|
-
|
|
37
|
+
const props = defineProps({
|
|
38
|
+
appBarProps: {
|
|
39
|
+
default: () => ({}),
|
|
40
|
+
type: Object
|
|
41
|
+
},
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
43
|
+
appMenuProps: {
|
|
44
|
+
default: () => ({}),
|
|
45
|
+
type: Object
|
|
58
46
|
},
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
...this.appBarProps,
|
|
64
|
-
...this.appMenuProps
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
showMenuDrawer () {
|
|
69
|
-
return !this.$qas.screen.untilLarge || this.menuDrawer
|
|
70
|
-
}
|
|
48
|
+
initialUnreadNotificationsCount: {
|
|
49
|
+
type: Number,
|
|
50
|
+
default: 0
|
|
71
51
|
},
|
|
72
52
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
53
|
+
modelValue: {
|
|
54
|
+
default: true,
|
|
55
|
+
type: Boolean
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const emit = defineEmits(['sign-out', 'update:modelValue'])
|
|
60
|
+
|
|
61
|
+
// expondo método "toggleNotificationsDrawer" para fora do componente.
|
|
62
|
+
defineExpose({ toggleNotificationsDrawer })
|
|
63
|
+
|
|
64
|
+
const screen = useScreen()
|
|
65
|
+
|
|
66
|
+
const { isNotificationsEnabled, setUnreadNotificationsCount } = useNotifications()
|
|
67
|
+
|
|
68
|
+
const menuDrawer = ref(false)
|
|
69
|
+
const notificationsDrawer = ref(false)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Como está sendo utilizado em um watcher com a propriedade 'immediate: true',
|
|
73
|
+
* é necessário criar a variável antes de atribuí-la ao watcher, para assim conseguir pará-lo.
|
|
74
|
+
*/
|
|
75
|
+
let unreadNotificationsCountWatcher = () => {}
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// computed
|
|
78
|
+
const defaultAppMenuProps = computed(() => {
|
|
79
|
+
return {
|
|
80
|
+
...props.appBarProps,
|
|
81
|
+
...props.appMenuProps
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const showMenuDrawer = computed(() => !screen.untilLarge || menuDrawer.value)
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A propriedade "initialUnreadNotificationsCount" é escutada apenas uma vez,
|
|
89
|
+
* quando ela é iniciada, seta o "unreadNotificationsCount" do composable,
|
|
90
|
+
* após isto quem controla é o QasLayout.
|
|
91
|
+
*/
|
|
92
|
+
unreadNotificationsCountWatcher = watch(() => props.initialUnreadNotificationsCount, value => {
|
|
93
|
+
if (value) {
|
|
94
|
+
setUnreadNotificationsCount(value)
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.$emit('update:modelValue', value)
|
|
85
|
-
}
|
|
96
|
+
// finaliza o watcher
|
|
97
|
+
unreadNotificationsCountWatcher()
|
|
86
98
|
}
|
|
99
|
+
}, { immediate: true })
|
|
100
|
+
|
|
101
|
+
// functions
|
|
102
|
+
function signOut () {
|
|
103
|
+
emit('sign-out')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toggleMenuDrawer () {
|
|
107
|
+
updateMenuDrawer(!menuDrawer.value)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function updateMenuDrawer (value) {
|
|
111
|
+
menuDrawer.value = value
|
|
112
|
+
|
|
113
|
+
emit('update:modelValue', value)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toggleNotificationsDrawer () {
|
|
117
|
+
notificationsDrawer.value = !notificationsDrawer.value
|
|
87
118
|
}
|
|
88
119
|
</script>
|
|
@@ -14,6 +14,11 @@ props:
|
|
|
14
14
|
default: { animation: 500 }
|
|
15
15
|
type: Object
|
|
16
16
|
|
|
17
|
+
initial-unread-notifications-count:
|
|
18
|
+
desc: Propriedade para indicar quantas notificações não lidas existem.
|
|
19
|
+
default: 0
|
|
20
|
+
type: Number
|
|
21
|
+
|
|
17
22
|
model-value:
|
|
18
23
|
desc: Model do componente, responsável por abrir/fechar menu lateral.
|
|
19
24
|
default: true
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="items-center justify-between no-wrap row">
|
|
3
|
+
<div class="items-center row">
|
|
4
|
+
<div class="q-mr-sm">
|
|
5
|
+
<q-icon :color="iconColor" name="sym_r_info" size="md" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div>
|
|
9
|
+
<span class="text-caption text-grey-6">
|
|
10
|
+
{{ dateLabel }}
|
|
11
|
+
</span>
|
|
12
|
+
|
|
13
|
+
<div class="items-center q-mt-xs row">
|
|
14
|
+
<h6 class="text-subtitle1" :class="titleClass">
|
|
15
|
+
{{ props.notification.title }}
|
|
16
|
+
</h6>
|
|
17
|
+
|
|
18
|
+
<div v-if="hasBadge" class="q-ml-sm">
|
|
19
|
+
<qas-badge color="indigo-1" label="Nova" text-color="grey-10" />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="q-mt-xs text-body1 text-grey-8">
|
|
24
|
+
{{ props.notification.message }}
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<qas-btn v-if="props.notification.link" v-bind="buttonProps" />
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup>
|
|
34
|
+
import { dateTime } from '../../../helpers/filters'
|
|
35
|
+
|
|
36
|
+
import { computed } from 'vue'
|
|
37
|
+
import { date } from 'quasar'
|
|
38
|
+
|
|
39
|
+
defineOptions({ name: 'PvLayoutNotificationCard' })
|
|
40
|
+
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
notification: {
|
|
43
|
+
type: Object,
|
|
44
|
+
default: () => ({})
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const markedAsRead = computed(() => props.notification.isRead)
|
|
49
|
+
|
|
50
|
+
const iconColor = computed(() => markedAsRead.value ? 'grey-8' : 'primary')
|
|
51
|
+
const titleClass = computed(() => markedAsRead.value ? 'text-grey-8' : 'text-grey-10')
|
|
52
|
+
|
|
53
|
+
const isRecentNotification = computed(() => {
|
|
54
|
+
const currentDate = new Date().toISOString()
|
|
55
|
+
|
|
56
|
+
return date.getDateDiff(currentDate, props.notification.createdAt, 'minutes') < 10
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const hasBadge = computed(() => isRecentNotification.value && !markedAsRead.value)
|
|
60
|
+
|
|
61
|
+
const dateLabel = computed(() => {
|
|
62
|
+
return isRecentNotification.value
|
|
63
|
+
? 'Agora mesmo'
|
|
64
|
+
: dateTime(props.notification.createdAt, 'dd/MM/yyyy HH:mm')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const buttonProps = computed(() => {
|
|
68
|
+
const { link } = props.notification
|
|
69
|
+
|
|
70
|
+
const urlFromLink = new URL(link)
|
|
71
|
+
|
|
72
|
+
const isExternalURL = urlFromLink.host !== location.host
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
icon: 'sym_r_chevron_right',
|
|
76
|
+
color: markedAsRead.value ? 'grey-10' : 'primary',
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Se for uma url externa, diferente da URL que ele esta acessando no momento,
|
|
80
|
+
* é adicionado um "href" porque é um link para outro modulo, caso seja para o mesmo modulo,
|
|
81
|
+
* é adicionado um "to" para não recarregar a pagina.
|
|
82
|
+
*/
|
|
83
|
+
...(isExternalURL ? { href: link } : { to: urlFromLink.pathname })
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
</script>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<qas-drawer v-bind="drawerProps" v-model="model">
|
|
3
|
+
<div class="fixed-position">
|
|
4
|
+
<div class="justify-end row">
|
|
5
|
+
<qas-btn class="q-mb-xl" :disable="isAllNotificationsRead" icon="sym_r_check_circle" label="Marcar todas como lida" @click="markAsRead" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<qas-infinite-scroll v-model:list="notifications" v-bind="infiniteScrollProps">
|
|
9
|
+
<qas-list-items :list="notifications" :use-box="false" :use-clickable-item="false" :use-section-actions="false">
|
|
10
|
+
<template #item-section="{ item }">
|
|
11
|
+
<pv-layout-notification-card :notification="item" />
|
|
12
|
+
</template>
|
|
13
|
+
</qas-list-items>
|
|
14
|
+
</qas-infinite-scroll>
|
|
15
|
+
</div>
|
|
16
|
+
</qas-drawer>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup>
|
|
20
|
+
import PvLayoutNotificationCard from './PvLayoutNotificationCard.vue'
|
|
21
|
+
import QasDrawer from '../../drawer/QasDrawer.vue'
|
|
22
|
+
import QasInfiniteScroll from '../../infinite-scroll/QasInfiniteScroll.vue'
|
|
23
|
+
|
|
24
|
+
import useNotifications, { onNotificationReceived } from '../../../composables/use-notifications'
|
|
25
|
+
|
|
26
|
+
import { promiseHandler } from '../../../helpers'
|
|
27
|
+
|
|
28
|
+
import { computed, ref, inject, onMounted } from 'vue'
|
|
29
|
+
|
|
30
|
+
defineOptions({ name: 'PvLayoutNotificationsDrawer' })
|
|
31
|
+
|
|
32
|
+
const props = defineProps({
|
|
33
|
+
model: {
|
|
34
|
+
type: Boolean
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits(['update:modelValue'])
|
|
39
|
+
|
|
40
|
+
const axios = inject('axios')
|
|
41
|
+
|
|
42
|
+
const { setUnreadNotificationsCount } = useNotifications()
|
|
43
|
+
|
|
44
|
+
const hasMadeFirstFetch = ref(false)
|
|
45
|
+
const isMarkingNotificationsAsRead = ref(false)
|
|
46
|
+
|
|
47
|
+
const notifications = ref([])
|
|
48
|
+
|
|
49
|
+
onMounted(() => {
|
|
50
|
+
const notificationsUtilsChannel = new BroadcastChannel('notifications--utils')
|
|
51
|
+
|
|
52
|
+
notificationsUtilsChannel.onmessage = ({ data }) => {
|
|
53
|
+
if (data.type === 'markAllAsRead') onMarkAsReadSuccess()
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Quando o usuário ainda não abriu a central de notificações, a primeira vez que
|
|
59
|
+
* ele abrir vai obter esses dados via API, mesmo que ele tenha recebido a notificação
|
|
60
|
+
* em real time, após ele ter aberto as notificações pelo menos uma vez, todas
|
|
61
|
+
* notificações recebidas em real time serão incrementadas na central de
|
|
62
|
+
* notificação sem a necessidade de chamar a API e resetar a paginação (feita por scroll).
|
|
63
|
+
*/
|
|
64
|
+
onNotificationReceived(notification => {
|
|
65
|
+
if (!hasMadeFirstFetch.value) return
|
|
66
|
+
|
|
67
|
+
notifications.value.unshift(notification)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const infiniteScrollProps = {
|
|
71
|
+
limitPerPage: 30,
|
|
72
|
+
// os "165px" são referentes ao cabeçalho.
|
|
73
|
+
maxHeight: 'calc(100vh - 165px)',
|
|
74
|
+
url: 'users/me/notifications',
|
|
75
|
+
onFetchSuccess
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// computed
|
|
79
|
+
const model = computed({
|
|
80
|
+
get () {
|
|
81
|
+
return props.model
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
set (value) {
|
|
85
|
+
emit('update:modelValue', value)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const drawerProps = computed(() => {
|
|
90
|
+
return {
|
|
91
|
+
loading: isMarkingNotificationsAsRead.value,
|
|
92
|
+
maxWidth: '60%',
|
|
93
|
+
title: 'Notificações'
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Se todas notificações estiverem lidas, então desabilitar o botão de "Marcar todas como lida"
|
|
99
|
+
*/
|
|
100
|
+
const isAllNotificationsRead = computed(() => !notifications.value.some(notification => !notification?.isRead))
|
|
101
|
+
|
|
102
|
+
// functions
|
|
103
|
+
async function markAsRead () {
|
|
104
|
+
const { data } = await promiseHandler(
|
|
105
|
+
axios.patch('/users/me/notifications', { markAllAsRead: true }),
|
|
106
|
+
{
|
|
107
|
+
useLoading: false,
|
|
108
|
+
errorMessage: 'Falha ao marcar todas notificações como lida. Por favor, tente novamente em alguns minutos.',
|
|
109
|
+
onLoading: isLoading => {
|
|
110
|
+
isMarkingNotificationsAsRead.value = isLoading
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (data) {
|
|
116
|
+
const notificationsUtilsChannel = new BroadcastChannel('notifications--utils')
|
|
117
|
+
|
|
118
|
+
notificationsUtilsChannel.postMessage({ type: 'markAllAsRead' })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Ao marcar todas notificações como lida, é necessário percorrer todo o array
|
|
124
|
+
* de "notifications" e setar a prop "isRead" como "true", para não precisar chamar
|
|
125
|
+
* novamente a API para atualizar estes dados.
|
|
126
|
+
*/
|
|
127
|
+
function onMarkAsReadSuccess () {
|
|
128
|
+
notifications.value.forEach(notification => {
|
|
129
|
+
notification.isRead = true
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Zera o contador de notificações, responsável pelo ícone no QasAppUser.
|
|
134
|
+
*/
|
|
135
|
+
setUnreadNotificationsCount(0)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function onFetchSuccess () {
|
|
139
|
+
hasMadeFirstFetch.value = true
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<component :is="component" class="qas-list-items" :class="classes">
|
|
3
3
|
<q-list separator>
|
|
4
4
|
<q-item v-for="(item, index) in props.list" :key="index" v-ripple :clickable="props.useClickableItem" @click="onClick({ item, index }, true)">
|
|
5
5
|
<slot :index="index" :item="item" name="item">
|
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
</slot>
|
|
16
16
|
</q-item>
|
|
17
17
|
</q-list>
|
|
18
|
-
</
|
|
18
|
+
</component>
|
|
19
19
|
</template>
|
|
20
20
|
|
|
21
21
|
<script setup>
|
|
22
|
-
import QasBox from '../box/QasBox.vue'
|
|
23
22
|
import QasBtn from '../btn/QasBtn.vue'
|
|
24
23
|
|
|
24
|
+
import { computed } from 'vue'
|
|
25
|
+
|
|
25
26
|
defineOptions({ name: 'QasListItems' })
|
|
26
27
|
|
|
27
28
|
const props = defineProps({
|
|
@@ -35,6 +36,11 @@ const props = defineProps({
|
|
|
35
36
|
type: Array
|
|
36
37
|
},
|
|
37
38
|
|
|
39
|
+
useBox: {
|
|
40
|
+
type: Boolean,
|
|
41
|
+
default: true
|
|
42
|
+
},
|
|
43
|
+
|
|
38
44
|
useClickableItem: {
|
|
39
45
|
type: Boolean
|
|
40
46
|
},
|
|
@@ -47,6 +53,10 @@ const props = defineProps({
|
|
|
47
53
|
|
|
48
54
|
const emit = defineEmits(['click-item'])
|
|
49
55
|
|
|
56
|
+
const classes = computed(() => ({ 'qas-list-items--no-click': !props.useClickableItem }))
|
|
57
|
+
|
|
58
|
+
const component = computed(() => props.useBox ? 'qas-box' : 'div')
|
|
59
|
+
|
|
50
60
|
function onClick ({ item, index }, fromItem) {
|
|
51
61
|
/**
|
|
52
62
|
* se o click veio do q-item e "useClickableItem" for "false", ou
|
|
@@ -62,8 +72,26 @@ function onClick ({ item, index }, fromItem) {
|
|
|
62
72
|
|
|
63
73
|
<style lang="scss">
|
|
64
74
|
.qas-list-items {
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
&--no-click {
|
|
76
|
+
.q-item {
|
|
77
|
+
.q-ripple {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.q-list {
|
|
84
|
+
& > .q-item {
|
|
85
|
+
padding: var(--qas-spacing-lg) 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
& > .q-item:last-child {
|
|
89
|
+
padding-bottom: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
& > .q-item:first-child {
|
|
93
|
+
padding-top: 0;
|
|
94
|
+
}
|
|
67
95
|
}
|
|
68
96
|
}
|
|
69
97
|
</style>
|
|
@@ -14,6 +14,11 @@ props:
|
|
|
14
14
|
default: []
|
|
15
15
|
type: Array
|
|
16
16
|
|
|
17
|
+
use-box:
|
|
18
|
+
desc: Controla se o vai ser um QasBox ou div.
|
|
19
|
+
type: Boolean
|
|
20
|
+
default: true
|
|
21
|
+
|
|
17
22
|
use-clickable-item:
|
|
18
23
|
desc: Controla se o item inteiro é clicável ou somente o button dentro do item.
|
|
19
24
|
type: Boolean
|
package/src/composables/index.js
CHANGED
|
@@ -3,3 +3,6 @@ export { default as useForm } from './use-form.js'
|
|
|
3
3
|
export { default as useHistory } from './use-history.js'
|
|
4
4
|
export { default as useQueryCache } from './use-query-cache.js'
|
|
5
5
|
export { default as useScreen } from './use-screen.js'
|
|
6
|
+
export { default as useNotifications } from './use-notifications.js'
|
|
7
|
+
|
|
8
|
+
export * from './use-notifications.js'
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import asteroidConfig from 'asteroid-config'
|
|
2
|
+
import hasParentByClassName from '../helpers/private/has-parent-by-class-name'
|
|
3
|
+
|
|
4
|
+
import { Notify } from 'quasar'
|
|
5
|
+
import { ref } from 'vue'
|
|
6
|
+
|
|
7
|
+
const callbackFunctions = {
|
|
8
|
+
onNotificationReceived: []
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function onNotificationReceived (callbackFn) {
|
|
12
|
+
callbackFunctions.onNotificationReceived.push(callbackFn)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const unreadNotificationsCount = ref(0)
|
|
16
|
+
|
|
17
|
+
export default function () {
|
|
18
|
+
const isNotificationsEnabled = asteroidConfig.framework.featureToggle?.useNotifications
|
|
19
|
+
|
|
20
|
+
function triggerNotification (notification) {
|
|
21
|
+
callbackFunctions.onNotificationReceived.forEach(fn => fn((notification)))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function setUnreadNotificationsCount (value) {
|
|
25
|
+
unreadNotificationsCount.value = value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function incrementUnreadNotificationsCount () {
|
|
29
|
+
unreadNotificationsCount.value++
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {{
|
|
34
|
+
* message: string
|
|
35
|
+
* title: string
|
|
36
|
+
* link?: string
|
|
37
|
+
* }} config
|
|
38
|
+
*/
|
|
39
|
+
function sendNotify (config = {}) {
|
|
40
|
+
const { link } = config
|
|
41
|
+
|
|
42
|
+
const classes = ['bg-white', 'boot-notification', 'q-py-sm', 'text-grey-8']
|
|
43
|
+
|
|
44
|
+
if (link) classes.push(...['boot-notification--has-link', 'cursor-pointer'])
|
|
45
|
+
|
|
46
|
+
const closeNotify = Notify.create({
|
|
47
|
+
actions: [{
|
|
48
|
+
icon: 'sym_r_close',
|
|
49
|
+
class: 'boot-notification__close-button qas-btn qas-btn--tertiary qas-btn--tertiary-grey-10'
|
|
50
|
+
}],
|
|
51
|
+
|
|
52
|
+
attrs: {
|
|
53
|
+
onClick: event => onNotifyClick({ event, link, closeNotify })
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
classes: classes.join(' '),
|
|
57
|
+
html: true,
|
|
58
|
+
icon: 'sym_r_info',
|
|
59
|
+
iconColor: 'primary',
|
|
60
|
+
message: getHTMLMessage(config),
|
|
61
|
+
multiLine: true,
|
|
62
|
+
position: 'bottom-right',
|
|
63
|
+
timeout: 30000
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Função que é chamada quando o usuário clica na notificação, se a notificação
|
|
68
|
+
* tem link, então ele vai ser redirecionado para o link em uma nova aba, caso
|
|
69
|
+
* não tenha link, nada acontece.
|
|
70
|
+
*/
|
|
71
|
+
function onNotifyClick ({ event, link, closeNotify }) {
|
|
72
|
+
if (!link) return
|
|
73
|
+
|
|
74
|
+
if (hasParentByClassName('boot-notification__close-button', event.srcElement)) return
|
|
75
|
+
|
|
76
|
+
window.open(link)
|
|
77
|
+
|
|
78
|
+
closeNotify()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Função que retorna um HTML para seguir estilos propostos pelo design.
|
|
83
|
+
*/
|
|
84
|
+
function getHTMLMessage ({ message, title, icon = 'info' } = {}) {
|
|
85
|
+
return (`
|
|
86
|
+
<div>
|
|
87
|
+
<header class="row items-center">
|
|
88
|
+
<h5 class="text-grey-10 text-h5 boot-notification__title">
|
|
89
|
+
${title}
|
|
90
|
+
</h5>
|
|
91
|
+
</header>
|
|
92
|
+
|
|
93
|
+
<div class="q-mt-sm text-grey-8 text-body1">
|
|
94
|
+
${message}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
// const
|
|
103
|
+
isNotificationsEnabled,
|
|
104
|
+
|
|
105
|
+
// computed
|
|
106
|
+
unreadNotificationsCount,
|
|
107
|
+
|
|
108
|
+
// functions
|
|
109
|
+
incrementUnreadNotificationsCount,
|
|
110
|
+
sendNotify,
|
|
111
|
+
setUnreadNotificationsCount,
|
|
112
|
+
triggerNotification
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
.q-notification {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
&:not(.boot-notification).qas-notification {
|
|
3
|
+
margin-top: 80px;
|
|
4
|
+
max-width: 560px;
|
|
5
|
+
}
|
|
4
6
|
|
|
5
7
|
&.qas-notification {
|
|
6
8
|
&--error::before,
|
|
@@ -56,6 +58,42 @@
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
&.boot-notification {
|
|
62
|
+
// classe do quasar adc fixo 10px de margin-bottom
|
|
63
|
+
margin-top: 0;
|
|
64
|
+
margin-bottom: calc(var(--qas-spacing-xl) - 10px);
|
|
65
|
+
max-width: 480px;
|
|
66
|
+
width: calc(100% - 20px);
|
|
67
|
+
|
|
68
|
+
&--has-link {
|
|
69
|
+
transition: var(--qas-generic-transition);
|
|
70
|
+
|
|
71
|
+
&:not(:has(.boot-notification__close-button:hover)):hover .boot-notification__title {
|
|
72
|
+
color: var(--q-primary) !important;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.boot-notification__close-button {
|
|
77
|
+
position: absolute;
|
|
78
|
+
right: calc(var(--qas-spacing-sm) * -1);
|
|
79
|
+
top: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.q-notification__wrapper {
|
|
83
|
+
position: relative;
|
|
84
|
+
|
|
85
|
+
&::before {
|
|
86
|
+
background-color: var(--q-primary);
|
|
87
|
+
bottom: calc((var(--qas-spacing-sm)) * -1);
|
|
88
|
+
content: '';
|
|
89
|
+
left: calc((var(--qas-spacing-md)) * -1);
|
|
90
|
+
position: absolute;
|
|
91
|
+
top: calc((var(--qas-spacing-sm)) * -1);
|
|
92
|
+
width: 4px;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
59
97
|
@media (max-width: $breakpoint-xs) {
|
|
60
98
|
margin-top: 40px;
|
|
61
99
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Função que retorna se o elemento passado é filho do elemento especificado
|
|
3
|
+
* pelo nome da classe.
|
|
4
|
+
*/
|
|
5
|
+
export default function (className, element) {
|
|
6
|
+
while (element.parentElement) {
|
|
7
|
+
const hasParentElement = element.parentElement.classList.contains(className)
|
|
8
|
+
|
|
9
|
+
if (hasParentElement) return true
|
|
10
|
+
|
|
11
|
+
element = element.parentElement
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return false
|
|
15
|
+
}
|
package/src/vue-plugin.js
CHANGED
|
@@ -19,6 +19,7 @@ import QasDebugger from './components/debugger/QasDebugger.vue'
|
|
|
19
19
|
import QasDelete from './components/delete/QasDelete.vue'
|
|
20
20
|
import QasDialog from './components/dialog/QasDialog.vue'
|
|
21
21
|
import QasDialogRouter from './components/dialog-router/QasDialogRouter.vue'
|
|
22
|
+
import QasDrawer from './components/drawer/QasDrawer.vue'
|
|
22
23
|
import QasEmptyResultText from './components/empty-result-text/QasEmptyResultText.vue'
|
|
23
24
|
import QasField from './components/field/QasField.vue'
|
|
24
25
|
import QasFilters from './components/filters/QasFilters.vue'
|
|
@@ -105,6 +106,7 @@ async function install (app) {
|
|
|
105
106
|
app.component('QasDelete', QasDelete)
|
|
106
107
|
app.component('QasDialog', QasDialog)
|
|
107
108
|
app.component('QasDialogRouter', QasDialogRouter)
|
|
109
|
+
app.component('QasDrawer', QasDrawer)
|
|
108
110
|
app.component('QasEmptyResultText', QasEmptyResultText)
|
|
109
111
|
app.component('QasField', QasField)
|
|
110
112
|
app.component('QasFilters', QasFilters)
|
|
@@ -193,6 +195,7 @@ export {
|
|
|
193
195
|
QasDelete,
|
|
194
196
|
QasDialog,
|
|
195
197
|
QasDialogRouter,
|
|
198
|
+
QasDrawer,
|
|
196
199
|
QasEmptyResultText,
|
|
197
200
|
QasField,
|
|
198
201
|
QasFilters,
|