@innertia-solutions/innertia-nuxt 0.1.11 → 0.1.13
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/CHANGELOG.md +38 -0
- package/app.config.ts +10 -10
- package/components/Attention.vue +269 -0
- package/components/Auth/LoginForm.vue +1 -1
- package/components/Chart/index.vue +193 -0
- package/components/Container.vue +116 -0
- package/components/CopyButton.vue +98 -0
- package/components/Files/Card.vue +215 -0
- package/components/Files/Preview.vue +675 -0
- package/components/Files/Upload.vue +408 -0
- package/components/Files/UploadModal.vue +83 -0
- package/components/Forms/InputPin.vue +162 -0
- package/components/InfoToolbar.vue +54 -0
- package/components/Kanban/Standard.vue +588 -0
- package/components/Loader.vue +358 -0
- package/components/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- package/components/Modal/Base.vue +6 -0
- package/components/Modal/Confirm.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +14 -31
- package/components/Modal/Form.vue +76 -0
- package/components/Modal/Host.vue +58 -0
- package/components/Modal/Prompt.vue +74 -0
- package/components/Modal.vue +119 -34
- package/components/Organization/Picker.vue +3 -3
- package/components/Table/Standard.vue +198 -25
- package/components/Table/index.vue +169 -369
- package/components/Table/views/ViewCards.vue +74 -0
- package/components/Table/views/ViewDatabase.vue +236 -0
- package/components/Table/views/ViewTable.vue +299 -0
- package/components/Toast/Alert.vue +16 -111
- package/components/Toast/Container.vue +4 -29
- package/components/Toast/Notification.vue +8 -43
- package/components/Toast/Process.vue +8 -86
- package/components/Toast/Stack.vue +59 -0
- package/components/Toast/Standard.vue +115 -0
- package/components/Tree/Standard.vue +49 -4
- package/components/Workflow/Board.vue +356 -0
- package/components/Workflow/Builder.vue +454 -0
- package/components/Workflow/Form.vue +526 -0
- package/components/Workflow/Viewer.vue +225 -0
- package/components/Workflow/edges/TransitionEdge.vue +84 -0
- package/components/Workflow/nodes/StepNode.vue +88 -0
- package/composables/useAuth.js +1 -1
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useContext.js +53 -5
- package/composables/useFile.js +189 -0
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useModal.js +71 -0
- package/composables/useOrganization.ts +3 -3
- package/composables/useToast.js +38 -65
- package/composables/useWorkflow/form.js +162 -0
- package/composables/useWorkflow/index.js +18 -0
- package/composables/useWorkflow/view.js +220 -0
- package/composables/useWorkflowLayout.js +162 -0
- package/css/tokens.css +10 -10
- package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
- package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
- package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
- package/composables/useApp.ts +0 -46
|
@@ -1,11 +1,46 @@
|
|
|
1
|
-
// useAuthStore, useApi, useAuth auto-imported
|
|
1
|
+
// useAuthStore, useApi, useAuth, useRoute, useAppConfig auto-imported
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
import type { ContextDefinition } from '../app.config'
|
|
3
4
|
|
|
4
5
|
export function useContext() {
|
|
6
|
+
const route = useRoute()
|
|
5
7
|
const authStore = useAuthStore()
|
|
8
|
+
const appConfig = useAppConfig()
|
|
6
9
|
const api = useApi()
|
|
7
10
|
const { fetchMe } = useAuth()
|
|
8
11
|
|
|
12
|
+
// ── Config-level: definiciones declaradas en appConfig.innertia.contexts ───
|
|
13
|
+
|
|
14
|
+
/** Diccionario de contextos declarados por el producto. */
|
|
15
|
+
const contexts = computed<Record<string, ContextDefinition>>(() =>
|
|
16
|
+
(appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
/** Todos los contextos declarados, en orden de declaración. */
|
|
20
|
+
const all = computed<ContextDefinition[]>(() => Object.values(contexts.value))
|
|
21
|
+
|
|
22
|
+
/** Contexto actual según URL. `null` si la URL no cae en ningún contexto declarado. */
|
|
23
|
+
const current = computed<ContextDefinition | null>(() => {
|
|
24
|
+
return all.value.find(ctx =>
|
|
25
|
+
route.path === ctx.path || route.path.startsWith(ctx.path + '/')
|
|
26
|
+
) ?? null
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/** Contextos a los que el usuario autenticado tiene acceso (filtrado por `availableContexts`). */
|
|
30
|
+
const accessible = computed<ContextDefinition[]>(() => {
|
|
31
|
+
const ctxs = (authStore.availableContexts ?? []) as string[]
|
|
32
|
+
return all.value.filter(ctx => ctxs.includes(ctx.context))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/** Helper: ¿el usuario puede acceder a este contexto (por clave de config)? */
|
|
36
|
+
function canAccess(key: string): boolean {
|
|
37
|
+
const ctx = contexts.value[key]
|
|
38
|
+
if (!ctx) return false
|
|
39
|
+
return ((authStore.availableContexts ?? []) as string[]).includes(ctx.context)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Runtime: contexto activo + switching ───────────────────────────────────
|
|
43
|
+
|
|
9
44
|
const currentContext = computed(() => authStore.currentContext)
|
|
10
45
|
const availableContexts = computed(() => authStore.availableContexts)
|
|
11
46
|
|
|
@@ -15,7 +50,7 @@ export function useContext() {
|
|
|
15
50
|
* { success: false, reason: 'no_permission' } — user cannot switch
|
|
16
51
|
* { success: true, requiresConfirmation: true } — show confirmation UI
|
|
17
52
|
*/
|
|
18
|
-
async function switchContext(targetContext) {
|
|
53
|
+
async function switchContext(targetContext: string) {
|
|
19
54
|
const data = await api.get(`auth/context/${targetContext}/check`)
|
|
20
55
|
if (!data.hasAccess) {
|
|
21
56
|
return { success: false, reason: 'no_permission' }
|
|
@@ -27,7 +62,7 @@ export function useContext() {
|
|
|
27
62
|
* Execute the context switch after user confirmation.
|
|
28
63
|
* Updates store and reloads permissions via fetchMe.
|
|
29
64
|
*/
|
|
30
|
-
async function confirmSwitch(targetContext) {
|
|
65
|
+
async function confirmSwitch(targetContext: string) {
|
|
31
66
|
authStore.setCurrentContext(targetContext)
|
|
32
67
|
await fetchMe()
|
|
33
68
|
return { success: true }
|
|
@@ -36,9 +71,22 @@ export function useContext() {
|
|
|
36
71
|
/**
|
|
37
72
|
* Quick synchronous check — is this context in the available list?
|
|
38
73
|
*/
|
|
39
|
-
function hasAccessToContext(context) {
|
|
74
|
+
function hasAccessToContext(context: string) {
|
|
40
75
|
return authStore.availableContexts.includes(context)
|
|
41
76
|
}
|
|
42
77
|
|
|
43
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
// config-level
|
|
80
|
+
contexts,
|
|
81
|
+
all,
|
|
82
|
+
current,
|
|
83
|
+
accessible,
|
|
84
|
+
canAccess,
|
|
85
|
+
// runtime
|
|
86
|
+
currentContext,
|
|
87
|
+
availableContexts,
|
|
88
|
+
switchContext,
|
|
89
|
+
confirmSwitch,
|
|
90
|
+
hasAccessToContext,
|
|
91
|
+
}
|
|
44
92
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconFile, IconPhoto, IconVideo, IconMusic, IconFileZip,
|
|
3
|
+
IconFileTypePdf, IconFileTypeXls, IconFileTypeCsv,
|
|
4
|
+
IconFileTypeDocx, IconFileTypePpt, IconFileTypeTxt,
|
|
5
|
+
} from '@tabler/icons-vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* useFile — API unificada para trabajar con archivos del backend innertia-laravel.
|
|
9
|
+
*
|
|
10
|
+
* Cubre:
|
|
11
|
+
* - upload(endpoint, file|files, { field, onProgress, extraData, signal })
|
|
12
|
+
* - download(fileId, filename?) — trigger descarga programática
|
|
13
|
+
* - view(fileId) — abrir inline en otra pestaña
|
|
14
|
+
* - viewUrl(fileId) / downloadUrl(fileId) — URLs absolutas a las rutas de innertia-laravel
|
|
15
|
+
* - formatSize(bytes) — helper humano-legible
|
|
16
|
+
* - iconFor(mimeType) — devuelve el componente Tabler para el mime
|
|
17
|
+
*
|
|
18
|
+
* Autenticación: los interceptores de useApi (auth Bearer, X-Tenant, X-Organization)
|
|
19
|
+
* se aplican automáticamente al XHR de upload.
|
|
20
|
+
*/
|
|
21
|
+
export function useFile() {
|
|
22
|
+
const config = useRuntimeConfig()
|
|
23
|
+
const baseUrl = config.public.apiBaseUrl || '/api'
|
|
24
|
+
|
|
25
|
+
// useRequestInterceptors es el mismo que usa useApi() — comparte auth/tenant/org headers.
|
|
26
|
+
const { run } = useRequestInterceptors()
|
|
27
|
+
|
|
28
|
+
// ── URLs ───────────────────────────────────────────────────────────────────
|
|
29
|
+
/** GET /files/{id} — inline view (respeta permisos del backend). */
|
|
30
|
+
const viewUrl = (fileId) => `${baseUrl}/files/${fileId}`
|
|
31
|
+
/** GET /files/{id}/download — force download (respeta permisos). */
|
|
32
|
+
const downloadUrl = (fileId) => `${baseUrl}/files/${fileId}/download`
|
|
33
|
+
|
|
34
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
35
|
+
const formatSize = (bytes) => {
|
|
36
|
+
if (bytes == null) return ''
|
|
37
|
+
if (bytes < 1024) return `${bytes} B`
|
|
38
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
39
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
40
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Devuelve el componente Tabler para un mime type. */
|
|
44
|
+
const iconFor = (mimeType) => {
|
|
45
|
+
if (!mimeType) return IconFile
|
|
46
|
+
if (mimeType.startsWith('image/')) return IconPhoto
|
|
47
|
+
if (mimeType.startsWith('video/')) return IconVideo
|
|
48
|
+
if (mimeType.startsWith('audio/')) return IconMusic
|
|
49
|
+
if (mimeType === 'application/pdf') return IconFileTypePdf
|
|
50
|
+
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return IconFileTypeXls
|
|
51
|
+
if (mimeType === 'text/csv') return IconFileTypeCsv
|
|
52
|
+
if (mimeType.includes('word') || mimeType.includes('document')) return IconFileTypeDocx
|
|
53
|
+
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return IconFileTypePpt
|
|
54
|
+
if (mimeType.startsWith('text/')) return IconFileTypeTxt
|
|
55
|
+
if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) return IconFileZip
|
|
56
|
+
return IconFile
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── View / Download ────────────────────────────────────────────────────────
|
|
60
|
+
/** Abre el file inline en una nueva pestaña (usa la auth de cookie). */
|
|
61
|
+
const view = (fileId) => {
|
|
62
|
+
if (typeof window === 'undefined') return
|
|
63
|
+
window.open(viewUrl(fileId), '_blank', 'noopener,noreferrer')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Trigger descarga programática. Funciona con archivos auth/restricted
|
|
68
|
+
* porque el browser envía cookies automáticamente.
|
|
69
|
+
*/
|
|
70
|
+
const download = (fileId, filename) => {
|
|
71
|
+
if (typeof window === 'undefined') return
|
|
72
|
+
const a = document.createElement('a')
|
|
73
|
+
a.href = downloadUrl(fileId)
|
|
74
|
+
if (filename) a.download = filename
|
|
75
|
+
document.body.appendChild(a)
|
|
76
|
+
a.click()
|
|
77
|
+
document.body.removeChild(a)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Upload (con progress vía XHR) ──────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Upload un archivo (o lista) a un endpoint del producto. El endpoint
|
|
83
|
+
* recibe `multipart/form-data` con el campo `field` y debe persistir el
|
|
84
|
+
* archivo (típicamente `File::fromRequest($request, 'file')` del lado Laravel).
|
|
85
|
+
*
|
|
86
|
+
* Para varios archivos pasa un array — el campo se envía como `field[]`.
|
|
87
|
+
* Devuelve el JSON parseado del response (típicamente el File record creado).
|
|
88
|
+
*
|
|
89
|
+
* Opciones:
|
|
90
|
+
* field — nombre del campo. Default 'file'.
|
|
91
|
+
* onProgress — callback(percent: number 0-100)
|
|
92
|
+
* extraData — campos adicionales para incluir en el FormData
|
|
93
|
+
* signal — AbortSignal para cancelar
|
|
94
|
+
*/
|
|
95
|
+
const upload = (endpoint, file, options = {}) => {
|
|
96
|
+
const field = options.field ?? 'file'
|
|
97
|
+
const files = Array.isArray(file) ? file : [file]
|
|
98
|
+
const formData = new FormData()
|
|
99
|
+
const fieldName = files.length > 1 ? `${field}[]` : field
|
|
100
|
+
|
|
101
|
+
for (const f of files) formData.append(fieldName, f)
|
|
102
|
+
|
|
103
|
+
if (options.extraData) {
|
|
104
|
+
for (const [k, v] of Object.entries(options.extraData)) {
|
|
105
|
+
if (v == null) continue
|
|
106
|
+
formData.append(k, typeof v === 'object' ? JSON.stringify(v) : String(v))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Recolectar headers vía los interceptores (auth, tenant, org).
|
|
111
|
+
// NO incluimos Content-Type — el browser lo setea con el boundary correcto.
|
|
112
|
+
const headers = {
|
|
113
|
+
'Accept': 'application/json',
|
|
114
|
+
'X-Innertia-Source': import.meta.server ? 'ssr' : 'client',
|
|
115
|
+
}
|
|
116
|
+
run(headers, {})
|
|
117
|
+
delete headers['Content-Type']
|
|
118
|
+
|
|
119
|
+
const cleanPath = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint
|
|
120
|
+
const url = `${baseUrl}/${cleanPath}`
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const xhr = new XMLHttpRequest()
|
|
124
|
+
xhr.open('POST', url)
|
|
125
|
+
xhr.withCredentials = true
|
|
126
|
+
|
|
127
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
128
|
+
try { xhr.setRequestHeader(k, v) } catch (_) { /* algunos headers son read-only */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
132
|
+
if (e.lengthComputable && options.onProgress) {
|
|
133
|
+
options.onProgress(Math.round((e.loaded / e.total) * 100), e.loaded, e.total)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Cuando el upload del cliente terminó (100%) pero el server aún no
|
|
138
|
+
// respondió — útil para mostrar estado "Procesando…" en la UI.
|
|
139
|
+
xhr.upload.addEventListener('loadend', () => {
|
|
140
|
+
options.onUploaded?.()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
xhr.addEventListener('load', () => {
|
|
144
|
+
const status = xhr.status
|
|
145
|
+
const ctype = xhr.getResponseHeader('content-type') ?? ''
|
|
146
|
+
const body = ctype.includes('application/json')
|
|
147
|
+
? safeJsonParse(xhr.responseText)
|
|
148
|
+
: xhr.responseText
|
|
149
|
+
|
|
150
|
+
if (status >= 200 && status < 300) {
|
|
151
|
+
resolve(body)
|
|
152
|
+
} else {
|
|
153
|
+
const err = new Error(`Upload failed (${status})`)
|
|
154
|
+
err.status = status
|
|
155
|
+
err.data = body
|
|
156
|
+
reject(err)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
xhr.addEventListener('error', () => reject(new Error('Network error during upload')))
|
|
161
|
+
xhr.addEventListener('abort', () => reject(Object.assign(new Error('Upload aborted'), { aborted: true })))
|
|
162
|
+
xhr.addEventListener('timeout', () => reject(new Error('Upload timeout')))
|
|
163
|
+
|
|
164
|
+
if (options.signal) {
|
|
165
|
+
if (options.signal.aborted) {
|
|
166
|
+
xhr.abort()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
options.signal.addEventListener('abort', () => xhr.abort())
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
xhr.send(formData)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
upload,
|
|
178
|
+
download,
|
|
179
|
+
view,
|
|
180
|
+
viewUrl,
|
|
181
|
+
downloadUrl,
|
|
182
|
+
formatSize,
|
|
183
|
+
iconFor,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function safeJsonParse(text) {
|
|
188
|
+
try { return JSON.parse(text) } catch { return text }
|
|
189
|
+
}
|
|
@@ -1,61 +1,61 @@
|
|
|
1
1
|
import { useMediaQuery } from '@vueuse/core'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ContextDefinition } from '../app.config'
|
|
3
3
|
|
|
4
|
-
const PICKER_CHOICE_COOKIE = '
|
|
4
|
+
const PICKER_CHOICE_COOKIE = 'innertia_mobile_context_choice'
|
|
5
5
|
const PICKER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 días
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Detección reactiva de viewport mobile + lógica de landing.
|
|
9
9
|
*
|
|
10
10
|
* Reglas para `landing` (qué mostrar cuando el usuario entra a `/` en mobile):
|
|
11
|
-
* - 0
|
|
12
|
-
* - 1
|
|
13
|
-
* - 2+
|
|
11
|
+
* - 0 contextos mobile-friendly → blocker "abre en escritorio"
|
|
12
|
+
* - 1 contexto mobile-friendly → redirect directo al login de ese contexto
|
|
13
|
+
* - 2+ contextos mobile-friendly → picker (con cookie para recordar última elección)
|
|
14
14
|
*
|
|
15
15
|
* El breakpoint se lee desde `appConfig.innertia.mobile.breakpoint` (default 1024).
|
|
16
16
|
*/
|
|
17
17
|
export function useMobileGuard() {
|
|
18
|
-
const { all, current, accessible } =
|
|
18
|
+
const { all, current, accessible } = useContext()
|
|
19
19
|
const authStore = useAuthStore()
|
|
20
20
|
const appConfig = useAppConfig()
|
|
21
21
|
|
|
22
22
|
const breakpoint: number = appConfig.innertia?.mobile?.breakpoint ?? 1024
|
|
23
23
|
const isMobile = useMediaQuery(`(max-width: ${breakpoint - 1}px)`)
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
const
|
|
25
|
+
/** Contextos con mode 'allow' en mobile. */
|
|
26
|
+
const mobileContexts = computed<ContextDefinition[]>(() =>
|
|
27
27
|
all.value.filter(a => a.mobile?.mode === 'allow')
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
const
|
|
30
|
+
/** Contextos mobile-friendly + accesibles para el usuario autenticado. */
|
|
31
|
+
const mobileAccessibleContexts = computed<ContextDefinition[]>(() =>
|
|
32
32
|
accessible.value.filter(a => a.mobile?.mode === 'allow')
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
/** ¿El
|
|
36
|
-
const
|
|
35
|
+
/** ¿El contexto actual está bloqueado en mobile? */
|
|
36
|
+
const isCurrentContextBlocked = computed<boolean>(() => {
|
|
37
37
|
if (!isMobile.value) return false
|
|
38
38
|
if (!current.value) return false
|
|
39
39
|
return current.value.mobile?.mode === 'block'
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Para el blocker: si el usuario está autenticado y tiene OTRO
|
|
43
|
+
* Para el blocker: si el usuario está autenticado y tiene OTRO contexto mobile-friendly,
|
|
44
44
|
* lo ofrecemos como fallback ("continuar en X").
|
|
45
45
|
*/
|
|
46
|
-
const
|
|
46
|
+
const mobileFallbackContext = computed<ContextDefinition | null>(() => {
|
|
47
47
|
if (!authStore.isAuthenticated()) return null
|
|
48
|
-
return
|
|
48
|
+
return mobileAccessibleContexts.value[0] ?? null
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
// ── Cookie helpers para recordar última elección del picker ─────────────────
|
|
52
|
-
function rememberPickerChoice(
|
|
52
|
+
function rememberPickerChoice(contextKey: string) {
|
|
53
53
|
if (!(appConfig.innertia?.mobile?.rememberChoice ?? true)) return
|
|
54
54
|
const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE, {
|
|
55
55
|
maxAge: PICKER_COOKIE_MAX_AGE,
|
|
56
56
|
sameSite: 'lax',
|
|
57
57
|
})
|
|
58
|
-
cookie.value =
|
|
58
|
+
cookie.value = contextKey
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function getRememberedPickerChoice(): string | null {
|
|
@@ -70,10 +70,10 @@ export function useMobileGuard() {
|
|
|
70
70
|
|
|
71
71
|
return {
|
|
72
72
|
isMobile,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
mobileContexts,
|
|
74
|
+
mobileAccessibleContexts,
|
|
75
|
+
isCurrentContextBlocked,
|
|
76
|
+
mobileFallbackContext,
|
|
77
77
|
rememberPickerChoice,
|
|
78
78
|
getRememberedPickerChoice,
|
|
79
79
|
clearRememberedPickerChoice,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// composables/useModal.js
|
|
2
|
+
import { useModalStore } from '../stores/modal'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* useModal() — API promise-based para modales programáticos.
|
|
6
|
+
*
|
|
7
|
+
* Requiere <ModalHost /> montado una vez en el layout root.
|
|
8
|
+
*
|
|
9
|
+
* ```js
|
|
10
|
+
* const { confirm, alert, prompt, open } = useModal()
|
|
11
|
+
*
|
|
12
|
+
* if (await confirm({ severity: 'danger', message: '¿Eliminar?' })) {
|
|
13
|
+
* // user confirmed
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* await alert({ severity: 'success', message: 'Listo' })
|
|
17
|
+
*
|
|
18
|
+
* const name = await prompt({ title: 'Nombre', placeholder: 'Tu nombre' })
|
|
19
|
+
*
|
|
20
|
+
* const result = await open(MyCustomModal, { foo: 'bar' })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useModal() {
|
|
24
|
+
const store = useModalStore()
|
|
25
|
+
|
|
26
|
+
/** Confirmación. Resuelve true/false. */
|
|
27
|
+
const confirm = (opts = {}) => store.open({
|
|
28
|
+
kind: 'confirm',
|
|
29
|
+
props: {
|
|
30
|
+
severity: opts.severity ?? 'question',
|
|
31
|
+
title: opts.title ?? '',
|
|
32
|
+
message: opts.message ?? '',
|
|
33
|
+
confirmText: opts.confirmText ?? 'Confirmar',
|
|
34
|
+
cancelText: opts.cancelText ?? 'Cancelar',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/** Alert simple — solo botón OK. Resuelve undefined. */
|
|
39
|
+
const alert = (opts = {}) => store.open({
|
|
40
|
+
kind: 'alert',
|
|
41
|
+
props: {
|
|
42
|
+
severity: opts.severity ?? 'info',
|
|
43
|
+
title: opts.title ?? '',
|
|
44
|
+
message: opts.message ?? '',
|
|
45
|
+
confirmText: opts.confirmText ?? 'Entendido',
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/** Prompt — pide un string. Resuelve string|null (null si cancela). */
|
|
50
|
+
const prompt = (opts = {}) => store.open({
|
|
51
|
+
kind: 'prompt',
|
|
52
|
+
props: {
|
|
53
|
+
title: opts.title ?? 'Ingresar valor',
|
|
54
|
+
message: opts.message ?? '',
|
|
55
|
+
placeholder: opts.placeholder ?? '',
|
|
56
|
+
defaultValue: opts.defaultValue ?? '',
|
|
57
|
+
confirmText: opts.confirmText ?? 'Aceptar',
|
|
58
|
+
cancelText: opts.cancelText ?? 'Cancelar',
|
|
59
|
+
required: opts.required ?? false,
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** Modal custom — montá tu propio componente. Resuelve con lo que pase a $close(value). */
|
|
64
|
+
const open = (component, props = {}) => store.open({
|
|
65
|
+
kind: 'component',
|
|
66
|
+
component,
|
|
67
|
+
props,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return { confirm, alert, prompt, open }
|
|
71
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Composable de organizaciones (sub-tenant scoping).
|
|
3
3
|
*
|
|
4
|
-
* Lee el contexto actual del usuario (via
|
|
4
|
+
* Lee el contexto actual del usuario (via useContext) y expone su org activa + lista
|
|
5
5
|
* de accesibles para ese contexto. Cambiar de contexto rota la org automáticamente.
|
|
6
6
|
*
|
|
7
7
|
* Uso típico:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* <OrganizationSwitcher v-if="available.length > 1" />
|
|
11
11
|
*/
|
|
12
12
|
export function useOrganization() {
|
|
13
|
-
const { current: currentApp } =
|
|
13
|
+
const { current: currentApp } = useContext()
|
|
14
14
|
const organizationStore = useOrganizationStore()
|
|
15
15
|
const appConfig = useAppConfig()
|
|
16
16
|
const { fetchMe } = useAuth()
|
|
@@ -19,7 +19,7 @@ export function useOrganization() {
|
|
|
19
19
|
const allowConsolidated = computed(() => !!appConfig.innertia?.organizations?.allowConsolidated)
|
|
20
20
|
const required = computed(() => !!appConfig.innertia?.organizations?.required)
|
|
21
21
|
|
|
22
|
-
/** Contexto actual (key) — desde
|
|
22
|
+
/** Contexto actual (key) — desde useContext. */
|
|
23
23
|
const currentContext = computed<string | null>(() => currentApp.value?.context ?? null)
|
|
24
24
|
|
|
25
25
|
/** Lista de orgs accesibles para el contexto actual. */
|
package/composables/useToast.js
CHANGED
|
@@ -1,69 +1,42 @@
|
|
|
1
|
-
//
|
|
1
|
+
// composables/useToast.js
|
|
2
2
|
import { useToastStore } from '../stores/toast'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* useToast() — API limpia para mostrar toasts.
|
|
6
|
+
*
|
|
7
|
+
* ```js
|
|
8
|
+
* const toast = useToast()
|
|
9
|
+
*
|
|
10
|
+
* toast.success('Guardado')
|
|
11
|
+
* toast.error('Algo falló', { title: 'Error', duration: 8000 })
|
|
12
|
+
* toast.warning('Atención')
|
|
13
|
+
* toast.info('Tip')
|
|
14
|
+
*
|
|
15
|
+
* toast.notify({ title: 'Nueva mención', message: 'Te etiquetaron', action: { label: 'Ver', onClick: () => {} } })
|
|
16
|
+
*
|
|
17
|
+
* const id = toast.process('Subiendo archivo…', { position: 'bottom-right' })
|
|
18
|
+
* toast.updateProgress(id, 40, '40%')
|
|
19
|
+
* toast.completeProcess(id, 'Listo')
|
|
20
|
+
*
|
|
21
|
+
* toast.dismiss(id)
|
|
22
|
+
* toast.clear() // todos
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
4
25
|
export function useToast() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type: 'alert',
|
|
22
|
-
message,
|
|
23
|
-
duration: 3000,
|
|
24
|
-
...config
|
|
25
|
-
}),
|
|
26
|
-
|
|
27
|
-
error: (message, config = {}) => toast.error({
|
|
28
|
-
type: 'alert',
|
|
29
|
-
message,
|
|
30
|
-
duration: 5000,
|
|
31
|
-
...config
|
|
32
|
-
}),
|
|
33
|
-
|
|
34
|
-
warning: (message, config = {}) => toast.show({
|
|
35
|
-
type: 'alert',
|
|
36
|
-
severity: 'warning',
|
|
37
|
-
icon: 'ti ti-alert-triangle',
|
|
38
|
-
title: 'Warning',
|
|
39
|
-
message,
|
|
40
|
-
duration: 4000,
|
|
41
|
-
...config
|
|
42
|
-
}),
|
|
43
|
-
|
|
44
|
-
info: (message, config = {}) => toast.info({
|
|
45
|
-
type: 'alert',
|
|
46
|
-
message,
|
|
47
|
-
duration: 3000,
|
|
48
|
-
...config
|
|
49
|
-
})
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
notification: (title, message, config = {}) => toast.show({
|
|
53
|
-
type: 'notification',
|
|
54
|
-
title,
|
|
55
|
-
message,
|
|
56
|
-
duration: 0, // Las notificaciones no se auto-dismiss por defecto
|
|
57
|
-
...config
|
|
58
|
-
}),
|
|
59
|
-
|
|
60
|
-
process: (title, config = {}) => toast.show({
|
|
61
|
-
type: 'process',
|
|
62
|
-
title,
|
|
63
|
-
progress: 0,
|
|
64
|
-
progressLabel: 'Iniciando...',
|
|
65
|
-
duration: 0, // Los procesos no se auto-dismiss
|
|
66
|
-
...config
|
|
67
|
-
})
|
|
68
|
-
}
|
|
26
|
+
const store = useToastStore()
|
|
27
|
+
return {
|
|
28
|
+
show: store.show.bind(store),
|
|
29
|
+
success: store.success.bind(store),
|
|
30
|
+
error: store.error.bind(store),
|
|
31
|
+
warning: store.warning.bind(store),
|
|
32
|
+
info: store.info.bind(store),
|
|
33
|
+
notify: store.notify.bind(store),
|
|
34
|
+
process: store.process.bind(store),
|
|
35
|
+
update: store.update.bind(store),
|
|
36
|
+
updateProgress: store.updateProgress.bind(store),
|
|
37
|
+
completeProcess: store.completeProcess.bind(store),
|
|
38
|
+
failProcess: store.failProcess.bind(store),
|
|
39
|
+
dismiss: store.dismiss.bind(store),
|
|
40
|
+
clear: store.clear.bind(store),
|
|
41
|
+
}
|
|
69
42
|
}
|