@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 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
+ }
@@ -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,11 @@
1
+ import { useHistory, setDefaultFiltersBeforeEach } from 'asteroid'
2
+
3
+ export default ({ router }) => {
4
+ router.beforeEach((to, from, next) => {
5
+ const { addRoute } = useHistory()
6
+
7
+ addRoute(to)
8
+
9
+ setDefaultFiltersBeforeEach(to, from, next)
10
+ })
11
+ }
@@ -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,8 @@
1
+ // TODO: Rever uso do build
2
+ // import VuePlugin from '@appnave/quasar-ui-asteroid/dist/asteroid.esm.js'
3
+
4
+ import { install } from 'asteroid'
5
+
6
+ export default ({ app }) => {
7
+ app.use({ install })
8
+ }
@@ -0,0 +1,5 @@
1
+ import { InitializeGlobalStores } from '@bildvitta/store-adapter'
2
+
3
+ export default ({ app }) => {
4
+ app.use(InitializeGlobalStores)
5
+ }
@@ -0,0 +1,8 @@
1
+ import asteroidConfigApp from 'asteroid-config-app'
2
+
3
+ import defaultAsteroidConfig from '../templates/js/asteroid.config.js'
4
+
5
+ export default {
6
+ ...defaultAsteroidConfig,
7
+ ...asteroidConfigApp
8
+ }
@@ -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,3 @@
1
+ export default function () {
2
+ return []
3
+ }
@@ -0,0 +1,4 @@
1
+ // Quasar SCSS (& Sass) Variables - Asteroid
2
+ // --------------------------------------------------
3
+ // NÃO MODIFIQUE ESTE ARQUIVO COM AS CONFIGURAÇÕES DO ASTEROID PRÉ DEFINIDAS!
4
+ @import '@appnave/quasar-ui-asteroid/src/index';
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export default function (api) {
2
+ api.onExitLog('A extensão ASTEROID foi desinstalada. 😢')
3
+ }