@appnave/quasar-app-extension-asteroid 3.20.0-beta.23
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 +48 -0
- package/src/boot/api.js +42 -0
- package/src/boot/before-each.js +11 -0
- package/src/boot/debug.js +10 -0
- package/src/boot/error-pages.js +24 -0
- package/src/boot/font-face.js +36 -0
- package/src/boot/loading.js +12 -0
- package/src/boot/notifications.js +45 -0
- package/src/boot/overlay-navigation.js +261 -0
- package/src/boot/query-cache.js +93 -0
- package/src/boot/register.js +8 -0
- package/src/boot/store-adapter.js +5 -0
- package/src/defaults/default-asteroid-config.js +8 -0
- package/src/helpers/asteroid-config-handler.js +22 -0
- package/src/helpers/laravel-echo.js +55 -0
- package/src/helpers/notifications-channels.js +36 -0
- package/src/helpers/on-leader-election-channel.js +23 -0
- package/src/index.js +159 -0
- package/src/index.scss +1 -0
- package/src/install.js +21 -0
- package/src/prompts.js +3 -0
- package/src/templates/css/quasar.variables.scss +4 -0
- package/src/templates/js/asteroid.config.js +44 -0
- package/src/uninstall.js +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appnave/quasar-app-extension-asteroid",
|
|
3
|
+
"description": "Asteroid",
|
|
4
|
+
"version": "3.20.0-beta.23",
|
|
5
|
+
"author": "Bild & Vitta <systemteam@bild.com.br>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public",
|
|
10
|
+
"registry": "https://registry.npmjs.org/"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"keys": [
|
|
14
|
+
"quasar",
|
|
15
|
+
"extension",
|
|
16
|
+
"vue",
|
|
17
|
+
"design-system",
|
|
18
|
+
"helpers",
|
|
19
|
+
"components"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/bildvitta/asteroid/"
|
|
24
|
+
},
|
|
25
|
+
"bugs": "https://github.com/bildvitta/asteroid/issues",
|
|
26
|
+
"homepage": "",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">= 8.9.0",
|
|
29
|
+
"npm": ">= 5.6.0",
|
|
30
|
+
"yarn": ">= 1.6.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@fawmi/vue-google-maps": "0.9.79",
|
|
34
|
+
"quasar": "^2.18.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@appnave/quasar-app-extension-asteroid": "file:",
|
|
38
|
+
"@appnave/quasar-ui-asteroid": "3.20.0-beta.23",
|
|
39
|
+
"@bildvitta/store-adapter": "^1.0.0",
|
|
40
|
+
"execa": "^7.1.1",
|
|
41
|
+
"fontfaceobserver": "^2.3.0",
|
|
42
|
+
"humps": "^2.0.1",
|
|
43
|
+
"laravel-echo": "^1.15.3",
|
|
44
|
+
"ora": "^8.2.0",
|
|
45
|
+
"pusher-js": "^8.4.0-rc2",
|
|
46
|
+
"unplugin-vue-components": "28.5.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/boot/api.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { camelizeKeys, decamelizeKeys } from 'humps'
|
|
2
|
+
|
|
3
|
+
import asteroidConfig from 'asteroid-config'
|
|
4
|
+
|
|
5
|
+
export default async ({ app }) => {
|
|
6
|
+
const api = app.config.globalProperties.$axios
|
|
7
|
+
|
|
8
|
+
// Defaults
|
|
9
|
+
api.defaults.baseURL = process.env.SERVER_BASE_URL || '/'
|
|
10
|
+
|
|
11
|
+
api.defaults.timeout = asteroidConfig.api.serverTimeout
|
|
12
|
+
|
|
13
|
+
// Transformers
|
|
14
|
+
api.defaults.transformResponse = [
|
|
15
|
+
...api.defaults.transformResponse,
|
|
16
|
+
|
|
17
|
+
data => camelizeKeys(data, (key, convert) =>
|
|
18
|
+
/^\D+$/.test(key) ? convert(key) : key
|
|
19
|
+
)
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
api.defaults.transformRequest = [
|
|
23
|
+
data => {
|
|
24
|
+
if (data instanceof FormData) {
|
|
25
|
+
return data
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return decamelizeKeys(data, (key, convert, options) =>
|
|
29
|
+
/^\D+$/.test(key) ? convert(key, options) : key
|
|
30
|
+
)
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
...api.defaults.transformRequest
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adicionado para que componentes que utilize o Composition API possam ter acesso a instancia do
|
|
38
|
+
* axios que contém as configurações necessárias da aplicação, uma vez que a utilização de provide
|
|
39
|
+
* é a forma recomendada para trabalhar com variavéis globais.
|
|
40
|
+
*/
|
|
41
|
+
app.provide('axios', api)
|
|
42
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { handleProcess } from 'asteroid'
|
|
2
|
+
|
|
3
|
+
import debug from 'debug'
|
|
4
|
+
|
|
5
|
+
export default async () => {
|
|
6
|
+
const debuggingEnv = handleProcess(() => process.env.DEBUGGING, false)
|
|
7
|
+
const debugEnv = handleProcess(() => process.env.DEBUG, '')
|
|
8
|
+
|
|
9
|
+
debug.enable(debuggingEnv && !debugEnv ? 'asteroid-*:*' : debugEnv)
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export default function ({ router }) {
|
|
2
|
+
const routes = [
|
|
3
|
+
{
|
|
4
|
+
name: 'Forbidden',
|
|
5
|
+
path: '/',
|
|
6
|
+
component: () => import('@appnave/quasar-ui-asteroid/src/pages/Forbidden.vue')
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'ServerError',
|
|
10
|
+
path: '/',
|
|
11
|
+
component: () => import('@appnave/quasar-ui-asteroid/src/pages/ServerError.vue')
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
if (process.env.MODE !== 'ssr') {
|
|
16
|
+
routes.push({
|
|
17
|
+
name: 'NotFound',
|
|
18
|
+
path: '/:catchAll(.*)*',
|
|
19
|
+
component: () => import('@appnave/quasar-ui-asteroid/src/pages/NotFound.vue')
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
routes.forEach(route => router.addRoute(route))
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import FontFaceObserver from 'fontfaceobserver'
|
|
2
|
+
import asteroidConfig from 'asteroid-config'
|
|
3
|
+
|
|
4
|
+
export default async () => {
|
|
5
|
+
function setFontFaceObserver () {
|
|
6
|
+
const font = new FontFaceObserver('Material Symbols Rounded')
|
|
7
|
+
|
|
8
|
+
const bodyClass = 'icons-is-loading'
|
|
9
|
+
|
|
10
|
+
const removeClass = () => document.body.classList.remove(bodyClass)
|
|
11
|
+
|
|
12
|
+
document.body.classList.add(bodyClass)
|
|
13
|
+
|
|
14
|
+
font.load(null, 60000).then(removeClass, removeClass)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fontFaceEventHandler (event) {
|
|
18
|
+
if (event.data.type === 'updateUser') {
|
|
19
|
+
setFontFaceObserver()
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* após escutar a primeira vez, já remove o listener para não executar novamente
|
|
23
|
+
* caso o evento "updateUser" seja executado novamente.
|
|
24
|
+
*/
|
|
25
|
+
window.removeEventListener('message', fontFaceEventHandler)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (asteroidConfig.framework?.fonts?.observer?.waitForUserAuthenticate) {
|
|
30
|
+
window.addEventListener('message', fontFaceEventHandler)
|
|
31
|
+
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setFontFaceObserver()
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Loading } from 'quasar'
|
|
2
|
+
|
|
3
|
+
export default () => {
|
|
4
|
+
Loading.setDefaults({
|
|
5
|
+
backgroundColor: 'white',
|
|
6
|
+
boxClass: 'text-body1',
|
|
7
|
+
customClass: 'qas-loading',
|
|
8
|
+
message: 'Carregando...',
|
|
9
|
+
messageColor: 'primary',
|
|
10
|
+
spinnerColor: 'primary'
|
|
11
|
+
})
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Este boot só é adicionado dinamicamente na aplicação caso a opção
|
|
3
|
+
* "asteroidConfig.framework.featureToggle.useNotifications" esteja ativada.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isLocalDevelopment } from 'asteroid'
|
|
7
|
+
|
|
8
|
+
import onLeaderElectionChannel from '../helpers/on-leader-election-channel.js'
|
|
9
|
+
import { setLaravelEcho, setLaravelEchoListener } from '../helpers/laravel-echo.js'
|
|
10
|
+
import { setNotificationsChannelListener, setNotificationsUtilsChannel } from '../helpers/notifications-channels.js'
|
|
11
|
+
|
|
12
|
+
import { LocalStorage } from 'quasar'
|
|
13
|
+
|
|
14
|
+
export default () => {
|
|
15
|
+
/**
|
|
16
|
+
* Se estivermos em desenvolvimento local, não vamos estabelecer conexão com o servidor.
|
|
17
|
+
*/
|
|
18
|
+
if (isLocalDevelopment()) return
|
|
19
|
+
|
|
20
|
+
window.addEventListener('message', ({ data }) => {
|
|
21
|
+
if (data.type !== 'updateUser') return
|
|
22
|
+
|
|
23
|
+
const user = data.user
|
|
24
|
+
const accessToken = LocalStorage.getItem('accessToken')
|
|
25
|
+
|
|
26
|
+
const hasBearerPrefix = accessToken.startsWith('Bearer ')
|
|
27
|
+
const userToken = hasBearerPrefix ? accessToken : `Bearer ${accessToken}`
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Aqui vamos estabelecer a conexão com o servidor apenas na tab (aba) líder.
|
|
31
|
+
* Vamos escutar por novas notificações, sempre que receber uma notificação,
|
|
32
|
+
* iremos enviar via BroadcastChannel.postMessage().
|
|
33
|
+
*/
|
|
34
|
+
onLeaderElectionChannel(channel => {
|
|
35
|
+
setLaravelEcho(userToken)
|
|
36
|
+
setLaravelEchoListener({ user, channel })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Controle de notificações por comunicação entre abas (BroadcastChannel).
|
|
41
|
+
*/
|
|
42
|
+
setNotificationsChannelListener()
|
|
43
|
+
setNotificationsUtilsChannel()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { useOverlayNavigation } from 'asteroid'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {import('vue-router').Router} router
|
|
5
|
+
*/
|
|
6
|
+
export default async function ({ router }) {
|
|
7
|
+
router.beforeEach((to, from, next) => onBeforeEach(to, from, next, router))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('vue-router').RouteLocationNormalized} to
|
|
12
|
+
* @param {import('vue-router').RouteLocationNormalized} from
|
|
13
|
+
* @param {Function} next
|
|
14
|
+
* @param {import('vue-router').Router} router
|
|
15
|
+
*/
|
|
16
|
+
async function onBeforeEach (to, from, next, router) {
|
|
17
|
+
const useOverlay = to.matched.some(item => item.meta.useOverlay)
|
|
18
|
+
|
|
19
|
+
if (!useOverlay) return next()
|
|
20
|
+
|
|
21
|
+
const { addRouteToHistory } = useOverlayNavigation()
|
|
22
|
+
|
|
23
|
+
addRouteToHistory(to, from)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Se houver mais de 2 níveis de rota, isto significa que esta rota tem 2 componentes pais, (sendo o primeiro o Root),
|
|
27
|
+
* nestes cenários, o componente overlay precisa ser o segundo componente pai, para renderizar de forma completa.
|
|
28
|
+
* Vamos supor o seguinte cenário: tenho uma lista de contatos, ao clicar em um contato, abro o overlay com o
|
|
29
|
+
* detalhe do contato, porém o detalhe do contato também possui um layout com rotas filhas, como "Resumo", neste
|
|
30
|
+
* cenário, o overlay precisa ser o componente pai do "detalhe" e não somente o resumo.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* [
|
|
34
|
+
* {
|
|
35
|
+
* "path": "/"
|
|
36
|
+
* },
|
|
37
|
+
* {
|
|
38
|
+
* "path": "/customers/:id" -> componente que precisa ser o overlay
|
|
39
|
+
* },
|
|
40
|
+
* {
|
|
41
|
+
* "path": "/customers/:id/summary"
|
|
42
|
+
* }
|
|
43
|
+
* ]
|
|
44
|
+
*/
|
|
45
|
+
const matchedIndex = to.matched.length > 1 ? 1 : 0
|
|
46
|
+
|
|
47
|
+
const { overlay, default: defaultComponent } = to.matched[matchedIndex]?.components || {}
|
|
48
|
+
|
|
49
|
+
const overlayComponent = await getResolvedComponent(overlay || defaultComponent)
|
|
50
|
+
|
|
51
|
+
// "overlay" vem como string na query da URL.
|
|
52
|
+
if (to.query.overlay === 'true') {
|
|
53
|
+
const backgroundResult = await getBackgroundComponent()
|
|
54
|
+
|
|
55
|
+
if (backgroundResult) {
|
|
56
|
+
const { resolvedRoute } = backgroundResult
|
|
57
|
+
|
|
58
|
+
const { name, params = {}, fullPath, path, query = {} } = resolvedRoute || {}
|
|
59
|
+
|
|
60
|
+
to.meta.backgroundRoute = { name, params, fullPath, path, query }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve todos os componentes lazy da rota de background e armazena a rota
|
|
64
|
+
* resolvida em `to.meta.overlayBackgroundResolvedRoute`.
|
|
65
|
+
*
|
|
66
|
+
* O QasLayout usa essa rota via `<router-view :route="..." />` para renderizar
|
|
67
|
+
* o background com sua hierarquia completa (parent layout + children),
|
|
68
|
+
* enquanto o overlay continua usando a rota atual normalmente.
|
|
69
|
+
*/
|
|
70
|
+
await resolveRouteComponents(resolvedRoute)
|
|
71
|
+
|
|
72
|
+
to.meta.overlayBackgroundResolvedRoute = resolvedRoute
|
|
73
|
+
|
|
74
|
+
// Apenas adicionar o overlay, sem alterar o default
|
|
75
|
+
to.matched[matchedIndex].components = {
|
|
76
|
+
...to.matched[matchedIndex].components,
|
|
77
|
+
overlay: overlayComponent
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
to.matched[matchedIndex].components = {
|
|
82
|
+
default: overlayComponent
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
next()
|
|
87
|
+
|
|
88
|
+
// functions
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve todos os componentes lazy (ex: () => import(...)) de uma rota.
|
|
92
|
+
* Necessário porque router.resolve() não resolve lazy components automaticamente.
|
|
93
|
+
*
|
|
94
|
+
* @param {import('vue-router').RouteLocationNormalized} route
|
|
95
|
+
*/
|
|
96
|
+
async function resolveRouteComponents (route) {
|
|
97
|
+
for (const matched of route.matched) {
|
|
98
|
+
for (const [viewName, comp] of Object.entries(matched.components || {})) {
|
|
99
|
+
matched.components[viewName] = await getResolvedComponent(comp)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getComponentByRoute (route) {
|
|
105
|
+
const lastIndex = route.matched.length - 1
|
|
106
|
+
const matched = route.matched[lastIndex]
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Busca o componente da rota. Pode vir em dois formatos:
|
|
110
|
+
*
|
|
111
|
+
* 1. `.components.default` - Quando:
|
|
112
|
+
* - A rota usa named views (múltiplos componentes)
|
|
113
|
+
* - A rota foi processada pelo overlay (criamos a estrutura { default, overlay })
|
|
114
|
+
* - Exemplo: { components: { default: MainComponent, sidebar: SidebarComponent } }
|
|
115
|
+
*
|
|
116
|
+
* 2. `.component` (sem 's') - Quando:
|
|
117
|
+
* - A rota tem um único componente simples
|
|
118
|
+
* - Lazy loading: component: () => import('./Component.vue')
|
|
119
|
+
* - Exemplo: { component: LoginComponent }
|
|
120
|
+
*/
|
|
121
|
+
const component = matched?.components?.default || matched?.component
|
|
122
|
+
|
|
123
|
+
return getResolvedComponent(component)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve um componente Vue, seja ele estático ou lazy-loaded.
|
|
128
|
+
*
|
|
129
|
+
* Componentes podem vir de duas formas:
|
|
130
|
+
* 1. **Estático**: Já importado - `LoginComponent`
|
|
131
|
+
* 2. **Lazy**: Função que retorna import - `() => import('./Login.vue')`
|
|
132
|
+
*
|
|
133
|
+
* Esta função garante que ambos os casos sejam tratados e retornem
|
|
134
|
+
* o componente pronto para uso.
|
|
135
|
+
*
|
|
136
|
+
* @param {Object|Function} component - O componente a ser resolvido
|
|
137
|
+
* @returns {Promise<Object>} Uma Promise que resolve com o componente Vue
|
|
138
|
+
*/
|
|
139
|
+
function getResolvedComponent (component) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
if (typeof component === 'function') {
|
|
142
|
+
component().then(module => resolve(module.default)).catch(error => reject(error))
|
|
143
|
+
} else {
|
|
144
|
+
resolve(component)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function getBackgroundComponent () {
|
|
150
|
+
const backgroundPath = to.query.backgroundOverlay
|
|
151
|
+
|
|
152
|
+
if (backgroundPath) {
|
|
153
|
+
try {
|
|
154
|
+
/**
|
|
155
|
+
* A url pode vir com query, ex: "/customers?tab=info", então precisamos separar a query para repassar depois.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* const normalizedURL = decodeURIComponent(backgroundPath) // "/customers?tab=info"
|
|
159
|
+
* const queryString = normalizedURL.split('?')[1] // "tab=info"
|
|
160
|
+
* const queryParams = normalizedURL ? new URLSearchParams(queryString) : {} // URLSearchParams { tab: 'info' }
|
|
161
|
+
*/
|
|
162
|
+
const normalizedURL = decodeURIComponent(backgroundPath)
|
|
163
|
+
const queryString = normalizedURL.split('?')[1]
|
|
164
|
+
const queryParams = normalizedURL ? new URLSearchParams(queryString) : {}
|
|
165
|
+
|
|
166
|
+
const queryObject = {}
|
|
167
|
+
|
|
168
|
+
queryParams.forEach((value, key) => {
|
|
169
|
+
queryObject[key] = value
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const resolvedRoute = router.resolve(normalizedURL)
|
|
173
|
+
|
|
174
|
+
const component = await getComponentByRoute(resolvedRoute)
|
|
175
|
+
|
|
176
|
+
if (component) {
|
|
177
|
+
return {
|
|
178
|
+
component,
|
|
179
|
+
resolvedRoute: {
|
|
180
|
+
...resolvedRoute,
|
|
181
|
+
query: queryObject,
|
|
182
|
+
params: resolvedRoute.params
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 2. Fallback: meta da rota atual
|
|
192
|
+
const backgroundOverlayName = to.meta?.backgroundOverlayName
|
|
193
|
+
|
|
194
|
+
if (backgroundOverlayName) {
|
|
195
|
+
try {
|
|
196
|
+
const resolvedRoute = router.resolve({ name: backgroundOverlayName })
|
|
197
|
+
|
|
198
|
+
const component = await getComponentByRoute(resolvedRoute)
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
component,
|
|
202
|
+
resolvedRoute: {
|
|
203
|
+
...resolvedRoute,
|
|
204
|
+
query: to.query,
|
|
205
|
+
params: {
|
|
206
|
+
...to.params,
|
|
207
|
+
...resolvedRoute.params
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 3. Fallback automático: rota base
|
|
218
|
+
*
|
|
219
|
+
* Tenta usar a rota "pai" como background quando não há outras opções.
|
|
220
|
+
*
|
|
221
|
+
* Pega o primeiro segmento da URL para criar uma rota base.
|
|
222
|
+
* Por exemplo: se estou em "/customers/123/details", tenta usar "/customers"
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* // URL atual: "/customers/123/edit"
|
|
226
|
+
* // Segments: ["customers", "123", "edit"]
|
|
227
|
+
* // Base path: "/customers" (primeiro segmento)
|
|
228
|
+
*/
|
|
229
|
+
const segments = to.path.split('/').filter(Boolean)
|
|
230
|
+
|
|
231
|
+
if (segments.length >= 2) {
|
|
232
|
+
const basePath = `/${segments[0]}`
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const resolvedRoute = router.resolve(basePath)
|
|
236
|
+
const component = await getComponentByRoute(resolvedRoute)
|
|
237
|
+
|
|
238
|
+
if (component) {
|
|
239
|
+
return {
|
|
240
|
+
component,
|
|
241
|
+
resolvedRoute: {
|
|
242
|
+
...resolvedRoute,
|
|
243
|
+
query: to.query,
|
|
244
|
+
params: {
|
|
245
|
+
...to.params,
|
|
246
|
+
...resolvedRoute.params
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 4. Fallback final
|
|
257
|
+
if (!from.name) return { component: overlayComponent, resolvedRoute: to }
|
|
258
|
+
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useQueryCache, useHistory } from 'asteroid'
|
|
2
|
+
|
|
3
|
+
const { addMany, findAll, clearAll } = useQueryCache()
|
|
4
|
+
|
|
5
|
+
let isReplacingQuery = false
|
|
6
|
+
|
|
7
|
+
function getDefaultUseCacheValue (route = {}) {
|
|
8
|
+
const { name, meta } = route
|
|
9
|
+
|
|
10
|
+
return meta.useCache ?? name?.toLowerCase?.().endsWith?.('list')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getQueriesFromMatchedRoutes (matched, key) {
|
|
14
|
+
const items = matched.reduce((acc, route) => {
|
|
15
|
+
const queries = route.meta[key] || []
|
|
16
|
+
|
|
17
|
+
if (queries) {
|
|
18
|
+
acc.push(...queries)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return acc
|
|
22
|
+
}, [])
|
|
23
|
+
|
|
24
|
+
return new Set(items)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getQueriesToExclude (route) {
|
|
28
|
+
const routes = route.matched || []
|
|
29
|
+
const excludes = getQueriesFromMatchedRoutes(routes, 'excludes').add('page')
|
|
30
|
+
const includes = getQueriesFromMatchedRoutes(routes, 'includes')
|
|
31
|
+
|
|
32
|
+
includes.forEach(value => excludes.delete(value))
|
|
33
|
+
|
|
34
|
+
return excludes
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setRouteCache (route) {
|
|
38
|
+
if (!getDefaultUseCacheValue(route)) return
|
|
39
|
+
|
|
40
|
+
let filteredQuery = {}
|
|
41
|
+
|
|
42
|
+
const { query } = route || {}
|
|
43
|
+
const queriesToExclude = getQueriesToExclude(route)
|
|
44
|
+
|
|
45
|
+
if (queriesToExclude.size) {
|
|
46
|
+
for (const item in query) {
|
|
47
|
+
if (!queriesToExclude.has(item)) {
|
|
48
|
+
filteredQuery[item] = query[item]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
filteredQuery = query
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const hasQueryParams = !!Object.keys(filteredQuery).length
|
|
56
|
+
|
|
57
|
+
if (!hasQueryParams) {
|
|
58
|
+
clearAll(route.name)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
addMany(route.name, filteredQuery)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default ({ router }) => {
|
|
66
|
+
router.beforeEach((to, from, next) => {
|
|
67
|
+
if (!useHistory().hasPreviousRoute.value) {
|
|
68
|
+
clearAll(to.name)
|
|
69
|
+
|
|
70
|
+
return next()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setRouteCache(from)
|
|
74
|
+
|
|
75
|
+
const { query } = to
|
|
76
|
+
const useCache = getDefaultUseCacheValue(to)
|
|
77
|
+
const hasQueries = !!Object.keys(query).length
|
|
78
|
+
const isSameRoute = to.name === from.name
|
|
79
|
+
const useCachedQuery = !isSameRoute && !hasQueries && useCache
|
|
80
|
+
|
|
81
|
+
if (useCachedQuery && !isReplacingQuery) {
|
|
82
|
+
const cachedQuery = findAll(to.name) || {}
|
|
83
|
+
|
|
84
|
+
isReplacingQuery = true
|
|
85
|
+
|
|
86
|
+
next({ ...to, query: cachedQuery })
|
|
87
|
+
} else {
|
|
88
|
+
isReplacingQuery = false
|
|
89
|
+
|
|
90
|
+
next()
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
|
|
4
|
+
export default function (api) {
|
|
5
|
+
const FILE_NAME = 'asteroid.config.js'
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
async validate () {
|
|
9
|
+
const hasAsteroidConfigFile = fs.existsSync(FILE_NAME)
|
|
10
|
+
|
|
11
|
+
if (!hasAsteroidConfigFile) {
|
|
12
|
+
ora('Você deve criar um arquivo asteroid.config.js na raiz do projeto').fail()
|
|
13
|
+
|
|
14
|
+
throw new Error()
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
getAsteroidConfigPath () {
|
|
19
|
+
return api.resolve.app(FILE_NAME)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isLocalDevelopment, handleProcess } from 'asteroid'
|
|
2
|
+
|
|
3
|
+
import Echo from 'laravel-echo'
|
|
4
|
+
import Pusher from 'pusher-js'
|
|
5
|
+
|
|
6
|
+
import { camelizeKeys } from 'humps'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Função para setar configuração do Laravel Echo.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} accessToken
|
|
12
|
+
*/
|
|
13
|
+
export function setLaravelEcho (accessToken) {
|
|
14
|
+
window.Pusher = Pusher
|
|
15
|
+
|
|
16
|
+
const isLocal = isLocalDevelopment()
|
|
17
|
+
|
|
18
|
+
window.Echo = new Echo({
|
|
19
|
+
broadcaster: 'pusher',
|
|
20
|
+
key: isLocal ? 'app-key' : handleProcess(() => process.env.ABLY_KEY, ''),
|
|
21
|
+
wsHost: isLocal ? 'localhost' : 'realtime-pusher.ably.io',
|
|
22
|
+
wsPort: isLocal ? 6001 : 443,
|
|
23
|
+
disableStats: true,
|
|
24
|
+
encrypted: true,
|
|
25
|
+
cluster: isLocal ? 'mt1' : 'eu',
|
|
26
|
+
authEndpoint: `${process.env.SERVER_BASE_URL}/broadcasting/auth`,
|
|
27
|
+
auth: {
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: accessToken
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Propriedades que só devem ser adicionas em localhost
|
|
34
|
+
...(isLocal && { wssPort: 6001, forceTLS: false })
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Função para setar o listener que vai enviar as mensagens via BroadcastChannel.postMessage
|
|
40
|
+
* para as demais abas.
|
|
41
|
+
*
|
|
42
|
+
* @param {{
|
|
43
|
+
* user: { uuid: string },
|
|
44
|
+
* channel: BroadcastChannel
|
|
45
|
+
* }} options
|
|
46
|
+
*/
|
|
47
|
+
export function setLaravelEchoListener ({ user, channel } = {}) {
|
|
48
|
+
window.Echo.private(`notifications.${user.uuid}`).listen('.notification', message => {
|
|
49
|
+
/**
|
|
50
|
+
* Como é um websocket, não passa pelo axios interceptor, logo é necessário
|
|
51
|
+
* transformar a resposta em camelCase manualmente.
|
|
52
|
+
*/
|
|
53
|
+
channel.postMessage({ notification: camelizeKeys(message) })
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useNotifications } from 'asteroid'
|
|
2
|
+
|
|
3
|
+
export function setNotificationsChannelListener () {
|
|
4
|
+
const {
|
|
5
|
+
incrementUnreadNotificationsCount,
|
|
6
|
+
sendNotify,
|
|
7
|
+
triggerNotification
|
|
8
|
+
} = useNotifications()
|
|
9
|
+
|
|
10
|
+
const notificationsChannel = new BroadcastChannel('notifications')
|
|
11
|
+
|
|
12
|
+
notificationsChannel.onmessage = ({ data: { notification } }) => {
|
|
13
|
+
// aciona o hook "onNotificationReceived".
|
|
14
|
+
triggerNotification(notification)
|
|
15
|
+
|
|
16
|
+
// dispara o "Notify" do Quasar.
|
|
17
|
+
sendNotify(notification)
|
|
18
|
+
|
|
19
|
+
// incrementa o contador "unreadNotificationsCount".
|
|
20
|
+
incrementUnreadNotificationsCount()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setNotificationsUtilsChannel () {
|
|
25
|
+
const { setUnreadNotificationsCount } = useNotifications()
|
|
26
|
+
|
|
27
|
+
const notificationsUtilsChannel = new BroadcastChannel('notifications--utils')
|
|
28
|
+
|
|
29
|
+
notificationsUtilsChannel.onmessage = ({ data: { type } }) => {
|
|
30
|
+
/**
|
|
31
|
+
* Se o botão "Marcar todas como lida" for acionado, então é zerado o "unreadNotificationsCount"
|
|
32
|
+
* em todas as abas.
|
|
33
|
+
*/
|
|
34
|
+
if (type === 'markAllAsRead') setUnreadNotificationsCount(0)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Esta função é utilizada para estabelecer um padrão de 'leader election'
|
|
3
|
+
* em um contexto de múltiplas abas ou workers.
|
|
4
|
+
* Ela utiliza a API 'navigator.locks.request' para garantir que apenas uma instância
|
|
5
|
+
* (aba/worker) atue como líder.
|
|
6
|
+
* A função recebe um callback que é executado apenas pela instância líder.
|
|
7
|
+
*
|
|
8
|
+
* @param {function(BroadcastChannel): void} callbackFn
|
|
9
|
+
*/
|
|
10
|
+
export default function onLeaderElection (callbackFn) {
|
|
11
|
+
const notificationsChannel = new BroadcastChannel('notifications')
|
|
12
|
+
|
|
13
|
+
navigator.locks.request('leader-election', () => {
|
|
14
|
+
callbackFn(notificationsChannel)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Para que o leader election funcione, é necessário retornar uma Promise
|
|
18
|
+
* que nunca será resolvida, é necessário também declarar o callback "resolve",
|
|
19
|
+
* por mais que ele não seja utilizado.
|
|
20
|
+
*/
|
|
21
|
+
return new Promise(_resolve => {})
|
|
22
|
+
})
|
|
23
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import asteroidConfigHandler from './helpers/asteroid-config-handler.js'
|
|
2
|
+
|
|
3
|
+
import ComponentsVite from 'unplugin-vue-components/vite'
|
|
4
|
+
import ComponentsWebpack from 'unplugin-vue-components/webpack'
|
|
5
|
+
import { pathToFileURL } from 'url'
|
|
6
|
+
|
|
7
|
+
const sourcePath = '~@appnave/quasar-app-extension-asteroid/src/'
|
|
8
|
+
const resolve = (...paths) => paths.map(path => sourcePath + path)
|
|
9
|
+
|
|
10
|
+
function extendQuasar (quasar, api, asteroidConfigFile) {
|
|
11
|
+
// Arquivos de boot
|
|
12
|
+
// https://quasar.dev/quasar-cli-vite/boot-files#introduction
|
|
13
|
+
quasar.boot.push(...resolve(
|
|
14
|
+
'boot/overlay-navigation.js',
|
|
15
|
+
'boot/api.js',
|
|
16
|
+
'boot/debug.js',
|
|
17
|
+
'boot/error-pages.js',
|
|
18
|
+
'boot/font-face.js',
|
|
19
|
+
'boot/register.js',
|
|
20
|
+
'boot/loading.js',
|
|
21
|
+
'boot/query-cache.js',
|
|
22
|
+
'boot/store-adapter',
|
|
23
|
+
'boot/before-each.js'
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
// controle das notificações
|
|
27
|
+
if (asteroidConfigFile.framework.featureToggle.useNotifications) {
|
|
28
|
+
quasar.boot.push(...resolve('boot/notifications'))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Transpilação de arquivos!
|
|
32
|
+
if (api.hasWebpack) {
|
|
33
|
+
const transpileTarget = (
|
|
34
|
+
quasar.build.webpackTranspileDependencies || // q/app-webpack >= v4
|
|
35
|
+
quasar.build.transpileDependencies // q/app-webpack v3
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
transpileTarget.push(
|
|
39
|
+
/quasar-app-extension-asteroid[\\/]src/,
|
|
40
|
+
/@bildvitta[\\/]quasar-ui-asteroid[\\/]src/
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Adiciona todas classes do asteroid
|
|
45
|
+
quasar.css.push(...resolve('index.scss'))
|
|
46
|
+
|
|
47
|
+
// Adiciona todos os Plugins obrigatório do Quasar
|
|
48
|
+
const plugins = [
|
|
49
|
+
'Dialog',
|
|
50
|
+
'Loading',
|
|
51
|
+
'Notify'
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
plugins.forEach(plugin => quasar.framework.plugins.push(plugin))
|
|
55
|
+
|
|
56
|
+
// Adiciona todas as classes de animação do Animate.css ao quasar
|
|
57
|
+
// https://animate.style/
|
|
58
|
+
const animations = [
|
|
59
|
+
'slideInDown',
|
|
60
|
+
'rubberBand',
|
|
61
|
+
'fadeIn'
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
animations.forEach(animation => quasar.animations.push(animation))
|
|
65
|
+
|
|
66
|
+
// Configurações
|
|
67
|
+
quasar.extras.push(
|
|
68
|
+
'material-symbols-rounded'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
quasar.framework.iconSet = 'material-symbols-rounded'
|
|
72
|
+
quasar.framework.lang = 'pt-BR'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default async function (api) {
|
|
76
|
+
api.compatibleWith('quasar', '^2.0.0')
|
|
77
|
+
api.compatibleWith('date-fns', '^2.3.0')
|
|
78
|
+
|
|
79
|
+
const asteroid = 'node_modules/@appnave/quasar-ui-asteroid/src/asteroid.js'
|
|
80
|
+
const asteroidComponents = 'node_modules/@appnave/quasar-ui-asteroid/src/components'
|
|
81
|
+
const asteroidConfig = 'node_modules/@appnave/quasar-app-extension-asteroid/src/defaults/default-asteroid-config.js'
|
|
82
|
+
const vueRouter = 'node_modules/vue-router/dist/vue-router.esm-bundler.js'
|
|
83
|
+
const quasar = 'node_modules/quasar'
|
|
84
|
+
|
|
85
|
+
const { validate, getAsteroidConfigPath } = asteroidConfigHandler(api)
|
|
86
|
+
|
|
87
|
+
// valida se existe o arquivo de configuração do asteroid "asteroid.config.js"
|
|
88
|
+
validate()
|
|
89
|
+
|
|
90
|
+
const asteroidConfigPath = getAsteroidConfigPath()
|
|
91
|
+
const { default: asteroidConfigFile } = await import(pathToFileURL(asteroidConfigPath).href)
|
|
92
|
+
|
|
93
|
+
const unpluginVueComponentsConfig = {
|
|
94
|
+
dirs: [api.resolve.app(asteroidComponents)], // ajusta o path para a lib
|
|
95
|
+
extensions: ['vue'],
|
|
96
|
+
deep: true,
|
|
97
|
+
dts: false // desativa geração de types
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const alias = {
|
|
101
|
+
'asteroid-config': api.resolve.app(asteroidConfig),
|
|
102
|
+
'asteroid-config-app': asteroidConfigPath,
|
|
103
|
+
'vue-router': api.resolve.app(vueRouter),
|
|
104
|
+
asteroid: api.resolve.app(asteroid),
|
|
105
|
+
quasar: api.resolve.app(quasar)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (api.hasVite) {
|
|
109
|
+
api.compatibleWith('@quasar/app-vite', '^2.0.0')
|
|
110
|
+
|
|
111
|
+
api.extendViteConf(viteConf => {
|
|
112
|
+
Object.assign(viteConf.resolve.alias, alias)
|
|
113
|
+
|
|
114
|
+
// optimizeDeps (necessário para funcionamento do QasMap)
|
|
115
|
+
viteConf.optimizeDeps = viteConf.optimizeDeps || {}
|
|
116
|
+
viteConf.optimizeDeps.include = viteConf.optimizeDeps.include || []
|
|
117
|
+
viteConf.optimizeDeps.include.push(...[
|
|
118
|
+
'@fawmi/vue-google-maps',
|
|
119
|
+
'fast-deep-equal',
|
|
120
|
+
'humps',
|
|
121
|
+
'debug',
|
|
122
|
+
'pica',
|
|
123
|
+
'hammerjs',
|
|
124
|
+
'lodash-es',
|
|
125
|
+
'date-fns',
|
|
126
|
+
'date-fns/locale'
|
|
127
|
+
])
|
|
128
|
+
|
|
129
|
+
viteConf.plugins = viteConf.plugins || []
|
|
130
|
+
viteConf.plugins.push(ComponentsVite(unpluginVueComponentsConfig))
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
api.extendQuasarConf(quasar => extendQuasar(quasar, api, asteroidConfigFile))
|
|
134
|
+
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
api.compatibleWith('@quasar/app', '^3.10.0 || ^4.0.0')
|
|
139
|
+
|
|
140
|
+
api.extendWebpack(webpack => {
|
|
141
|
+
Object.assign(webpack.resolve.alias, alias)
|
|
142
|
+
|
|
143
|
+
// Adiciona o plugin de componentes
|
|
144
|
+
webpack.plugins = webpack.plugins || []
|
|
145
|
+
webpack.plugins.push(ComponentsWebpack(unpluginVueComponentsConfig))
|
|
146
|
+
|
|
147
|
+
// Resolve o conflito de alias do "images" usado na lib leaflet.
|
|
148
|
+
webpack.module.rules.push({
|
|
149
|
+
test: /leaflet[\\/]dist[\\/].*\.css$/,
|
|
150
|
+
resolve: {
|
|
151
|
+
alias: {
|
|
152
|
+
images: api.resolve.app('node_modules/leaflet/dist/images')
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
api.extendQuasarConf(quasar => extendQuasar(quasar, api, asteroidConfigFile))
|
|
159
|
+
}
|
package/src/index.scss
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import '@appnave/quasar-ui-asteroid/src/index';
|
package/src/install.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default function (api) {
|
|
2
|
+
api.onExitLog(`
|
|
3
|
+
_ _ _
|
|
4
|
+
__ _ __| |_ ___ _ _ ___(_)__| |
|
|
5
|
+
/ _\` (_-< _/ -_) '_/ _ \\ / _\` |
|
|
6
|
+
\\__,_/__/\\__\\___|_| \\___/_\\__,_|
|
|
7
|
+
~+ INSTALAÇÃO CONCLUÍDA +~
|
|
8
|
+
`)
|
|
9
|
+
|
|
10
|
+
// -------------------------- CSS: quasar.variables.scss --------------------------
|
|
11
|
+
api.renderFile(
|
|
12
|
+
'./templates/css/quasar.variables.scss', // caminho do arquivo importado
|
|
13
|
+
'src/css/quasar.variables.scss' // caminho do arquivo que será criado no projeto
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// -------------------------- JS: asteroid.config.js -------------------------------
|
|
17
|
+
api.renderFile(
|
|
18
|
+
'./templates/js/asteroid.config.js', // caminho do arquivo importado
|
|
19
|
+
'asteroid.config.js' // caminho do arquivo que será criado no projeto
|
|
20
|
+
)
|
|
21
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
/**
|
|
3
|
+
* Configurações de API
|
|
4
|
+
* @type {{ serverTimeout: number=10000 }}
|
|
5
|
+
*/
|
|
6
|
+
api: {
|
|
7
|
+
serverTimeout: 10000
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
framework: {
|
|
11
|
+
fonts: {
|
|
12
|
+
observer: {
|
|
13
|
+
/**
|
|
14
|
+
* O asteroid adiciona um observer nas fontes, enquanto ela não carrega adiciona um loading para
|
|
15
|
+
* não quebrar o layout, a configuração "waitForUserAuthenticate: true" define que o observer deve esperar
|
|
16
|
+
* o usuário estar autenticado para começar a observar as fontes, para isto a aplicação deve poder receber um
|
|
17
|
+
* "postMessage" com um evento type "requestUser" e responder com um evento type "responseUser".
|
|
18
|
+
*
|
|
19
|
+
* @type {boolean=true}
|
|
20
|
+
* @example
|
|
21
|
+
* // solicita o usuário
|
|
22
|
+
* window.postMessage({ type: 'requestUser' })
|
|
23
|
+
*
|
|
24
|
+
* window.addEventListener('message', ({ data }) => data.type // responseUser })
|
|
25
|
+
*/
|
|
26
|
+
waitForUserAuthenticate: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Controla o sistema de notificações da aplicação, ao ativado e configurado,
|
|
32
|
+
* estará disponível os seguintes recursos:
|
|
33
|
+
*
|
|
34
|
+
* - Item de notificação no componente QasAppUser;
|
|
35
|
+
* - Toast de notificação em real time;
|
|
36
|
+
* - Ícone de notificação no menu em real time.
|
|
37
|
+
*
|
|
38
|
+
* @type {{ useNotifications: boolean }}
|
|
39
|
+
*/
|
|
40
|
+
featureToggle: {
|
|
41
|
+
useNotifications: false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/uninstall.js
ADDED