@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
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
# innertia-nuxt: apps → contexts Rename — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Rename the "apps" concept to "contexts" throughout innertia-nuxt, aligning the frontend library with the already-renamed innertia-laravel backend.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Pure rename — zero behavior changes. Config key `innertia.apps` → `innertia.contexts`, types `AppDefinition`/`AppMobileMode` → `ContextDefinition`/`ContextMobileMode`, composable `useApp()` merged into `useContext()`, middleware file renamed, login body field corrected. No backward-compat aliases.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Nuxt 4, Vue 3, TypeScript, Pinia. No test suite in this repo — verification is via `grep` for stale references.
|
|
10
|
+
|
|
11
|
+
**Working directory:** `/Users/guillermofarias/Innertia/Dev/workspace/libraries/innertia-nuxt`
|
|
12
|
+
|
|
13
|
+
**Spec:** `docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File Map
|
|
18
|
+
|
|
19
|
+
| File | Action |
|
|
20
|
+
|------|--------|
|
|
21
|
+
| `app.config.ts` | Rename type `AppDefinition` → `ContextDefinition`, `AppMobileMode` → `ContextMobileMode`, config key `innertia.apps` → `innertia.contexts` |
|
|
22
|
+
| `composables/useContext.js` | Absorb all logic from `useApp.ts`; now also reads from `innertia.contexts` and returns `current`, `all`, `accessible`, `canAccess`, `contexts` |
|
|
23
|
+
| `composables/useApp.ts` | **Delete** |
|
|
24
|
+
| `composables/useAuth.js` | Fix login body: `{ app: context }` → `{ context }` |
|
|
25
|
+
| `middleware/03.apps.global.ts` | **Rename** to `03.contexts.global.ts`; update import + internal variable references |
|
|
26
|
+
| `composables/useOrganization.ts` | `useApp()` → `useContext()` |
|
|
27
|
+
| `composables/useMobileGuard.ts` | `useApp()` → `useContext()`, `AppDefinition` → `ContextDefinition`, cookie key renamed |
|
|
28
|
+
| `CHANGELOG.md` | Create — document BREAKING CHANGES |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### Task 1: `app.config.ts` — rename types and config key
|
|
33
|
+
|
|
34
|
+
**Files:**
|
|
35
|
+
- Modify: `app.config.ts`
|
|
36
|
+
|
|
37
|
+
- [ ] **Step 1: Replace `innertia.apps` config key with `innertia.contexts`**
|
|
38
|
+
|
|
39
|
+
Find line (currently ~128):
|
|
40
|
+
```ts
|
|
41
|
+
apps: {} as Record<string, AppDefinition>,
|
|
42
|
+
```
|
|
43
|
+
Replace with:
|
|
44
|
+
```ts
|
|
45
|
+
/**
|
|
46
|
+
* Declaración de contextos del producto.
|
|
47
|
+
* Cada contexto define un prefijo de URL que mapea a un contexto del backend
|
|
48
|
+
* (matching con `availableContexts` que devuelve `auth/me`).
|
|
49
|
+
*/
|
|
50
|
+
contexts: {} as Record<string, ContextDefinition>,
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Also update the comment block just above it (currently ~122–127):
|
|
54
|
+
```ts
|
|
55
|
+
/**
|
|
56
|
+
* Declaración de "apps" (contextos) del producto.
|
|
57
|
+
* Cada app define un prefijo de URL que mapea a un contexto del backend
|
|
58
|
+
* (matching con `availableContexts` que devuelve `auth/me`).
|
|
59
|
+
*/
|
|
60
|
+
apps: {} as Record<string, AppDefinition>,
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- [ ] **Step 2: Rename `AppMobileMode` type**
|
|
64
|
+
|
|
65
|
+
Find line (~147):
|
|
66
|
+
```ts
|
|
67
|
+
export type AppMobileMode = 'allow' | 'block' | 'redirect'
|
|
68
|
+
```
|
|
69
|
+
Replace with:
|
|
70
|
+
```ts
|
|
71
|
+
export type ContextMobileMode = 'allow' | 'block' | 'redirect'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- [ ] **Step 3: Rename `AppDefinition` interface and update its internal reference to `AppMobileMode`**
|
|
75
|
+
|
|
76
|
+
Find (~178–199):
|
|
77
|
+
```ts
|
|
78
|
+
export interface AppDefinition {
|
|
79
|
+
/** Prefijo de URL — la ruta debe empezar con esto para considerarse "dentro" del app. */
|
|
80
|
+
path: string
|
|
81
|
+
/** Clave del contexto que matchea con `availableContexts` del backend. */
|
|
82
|
+
context: string
|
|
83
|
+
/** Label visible en pickers, dropdowns, etc. */
|
|
84
|
+
label: string
|
|
85
|
+
/** Descripción opcional para el picker mobile. */
|
|
86
|
+
description?: string
|
|
87
|
+
/** Nombre del icono de @tabler/icons-vue (sin "Icon"). */
|
|
88
|
+
icon?: string
|
|
89
|
+
/** Ruta de login del app. */
|
|
90
|
+
loginPath: string
|
|
91
|
+
/** Ruta home (post-login). */
|
|
92
|
+
home: string
|
|
93
|
+
/** Política de uso en mobile. */
|
|
94
|
+
mobile: {
|
|
95
|
+
mode: AppMobileMode
|
|
96
|
+
/** Solo si mode === 'redirect' — adónde mandar desde mobile. */
|
|
97
|
+
redirectTo?: string
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
Replace with:
|
|
102
|
+
```ts
|
|
103
|
+
export interface ContextDefinition {
|
|
104
|
+
/** Prefijo de URL — la ruta debe empezar con esto para considerarse "dentro" del contexto. */
|
|
105
|
+
path: string
|
|
106
|
+
/** Clave del contexto que matchea con `availableContexts` del backend. */
|
|
107
|
+
context: string
|
|
108
|
+
/** Label visible en pickers, dropdowns, etc. */
|
|
109
|
+
label: string
|
|
110
|
+
/** Descripción opcional para el picker mobile. */
|
|
111
|
+
description?: string
|
|
112
|
+
/** Nombre del icono de @tabler/icons-vue (sin "Icon"). */
|
|
113
|
+
icon?: string
|
|
114
|
+
/** Ruta de login del contexto. */
|
|
115
|
+
loginPath: string
|
|
116
|
+
/** Ruta home (post-login). */
|
|
117
|
+
home: string
|
|
118
|
+
/** Política de uso en mobile. */
|
|
119
|
+
mobile: {
|
|
120
|
+
mode: ContextMobileMode
|
|
121
|
+
/** Solo si mode === 'redirect' — adónde mandar desde mobile. */
|
|
122
|
+
redirectTo?: string
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- [ ] **Step 4: Verify no remaining `AppDefinition` or `AppMobileMode` references in `app.config.ts`**
|
|
128
|
+
|
|
129
|
+
Run:
|
|
130
|
+
```bash
|
|
131
|
+
grep -n "AppDefinition\|AppMobileMode" app.config.ts
|
|
132
|
+
```
|
|
133
|
+
Expected: no output (zero matches).
|
|
134
|
+
|
|
135
|
+
- [ ] **Step 5: Commit**
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git add app.config.ts
|
|
139
|
+
git commit -m "refactor(contexts): rename AppDefinition→ContextDefinition, AppMobileMode→ContextMobileMode, innertia.apps→innertia.contexts in app.config"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### Task 2: `useContext.js` — absorb `useApp` logic
|
|
145
|
+
|
|
146
|
+
**Files:**
|
|
147
|
+
- Modify: `composables/useContext.js`
|
|
148
|
+
|
|
149
|
+
The current `useContext.js` handles runtime context switching. We extend it to also expose the config-level definitions (previously in `useApp.ts`), reading from `appConfig.innertia.contexts`.
|
|
150
|
+
|
|
151
|
+
- [ ] **Step 1: Replace the full contents of `composables/useContext.js`**
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
// useAuthStore, useApi, useAuth auto-imported
|
|
155
|
+
import { computed } from 'vue'
|
|
156
|
+
import type { ContextDefinition } from '../app.config'
|
|
157
|
+
|
|
158
|
+
export function useContext() {
|
|
159
|
+
const route = useRoute()
|
|
160
|
+
const authStore = useAuthStore()
|
|
161
|
+
const appConfig = useAppConfig()
|
|
162
|
+
const api = useApi()
|
|
163
|
+
const { fetchMe } = useAuth()
|
|
164
|
+
|
|
165
|
+
// ── Config-level: definiciones declaradas en appConfig.innertia.contexts ───
|
|
166
|
+
|
|
167
|
+
/** Diccionario de contextos declarados por el producto. */
|
|
168
|
+
const contexts = computed<Record<string, ContextDefinition>>(() =>
|
|
169
|
+
(appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
/** Todos los contextos declarados, en orden de declaración. */
|
|
173
|
+
const all = computed<ContextDefinition[]>(() => Object.values(contexts.value))
|
|
174
|
+
|
|
175
|
+
/** Contexto actual según URL. `null` si la URL no cae en ningún contexto declarado. */
|
|
176
|
+
const current = computed<ContextDefinition | null>(() => {
|
|
177
|
+
return all.value.find(ctx =>
|
|
178
|
+
route.path === ctx.path || route.path.startsWith(ctx.path + '/')
|
|
179
|
+
) ?? null
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
/** Contextos a los que el usuario autenticado tiene acceso (filtrado por `availableContexts`). */
|
|
183
|
+
const accessible = computed<ContextDefinition[]>(() => {
|
|
184
|
+
const ctxs = (authStore.availableContexts ?? []) as string[]
|
|
185
|
+
return all.value.filter(ctx => ctxs.includes(ctx.context))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
/** Helper: ¿el usuario puede acceder a este contexto (por clave de config)? */
|
|
189
|
+
function canAccess(key: string): boolean {
|
|
190
|
+
const ctx = contexts.value[key]
|
|
191
|
+
if (!ctx) return false
|
|
192
|
+
return ((authStore.availableContexts ?? []) as string[]).includes(ctx.context)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Runtime: contexto activo + switching ───────────────────────────────────
|
|
196
|
+
|
|
197
|
+
const currentContext = computed(() => authStore.currentContext)
|
|
198
|
+
const availableContexts = computed(() => authStore.availableContexts)
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check whether user has permission to switch to targetContext.
|
|
202
|
+
* Returns:
|
|
203
|
+
* { success: false, reason: 'no_permission' } — user cannot switch
|
|
204
|
+
* { success: true, requiresConfirmation: true } — show confirmation UI
|
|
205
|
+
*/
|
|
206
|
+
async function switchContext(targetContext) {
|
|
207
|
+
const data = await api.get(`auth/context/${targetContext}/check`)
|
|
208
|
+
if (!data.hasAccess) {
|
|
209
|
+
return { success: false, reason: 'no_permission' }
|
|
210
|
+
}
|
|
211
|
+
return { success: true, requiresConfirmation: true }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Execute the context switch after user confirmation.
|
|
216
|
+
* Updates store and reloads permissions via fetchMe.
|
|
217
|
+
*/
|
|
218
|
+
async function confirmSwitch(targetContext) {
|
|
219
|
+
authStore.setCurrentContext(targetContext)
|
|
220
|
+
await fetchMe()
|
|
221
|
+
return { success: true }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Quick synchronous check — is this context in the available list?
|
|
226
|
+
*/
|
|
227
|
+
function hasAccessToContext(context) {
|
|
228
|
+
return authStore.availableContexts.includes(context)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
// config-level
|
|
233
|
+
contexts,
|
|
234
|
+
all,
|
|
235
|
+
current,
|
|
236
|
+
accessible,
|
|
237
|
+
canAccess,
|
|
238
|
+
// runtime
|
|
239
|
+
currentContext,
|
|
240
|
+
availableContexts,
|
|
241
|
+
switchContext,
|
|
242
|
+
confirmSwitch,
|
|
243
|
+
hasAccessToContext,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- [ ] **Step 2: Verify the file saved correctly**
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
grep -n "innertia.contexts\|ContextDefinition\|contexts\|current\|accessible" composables/useContext.js | head -20
|
|
252
|
+
```
|
|
253
|
+
Expected: lines showing `innertia.contexts`, `ContextDefinition`, and all the returned properties.
|
|
254
|
+
|
|
255
|
+
- [ ] **Step 3: Commit**
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
git add composables/useContext.js
|
|
259
|
+
git commit -m "refactor(contexts): useContext absorbs useApp — exposes current, all, accessible, canAccess, contexts from innertia.contexts config"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### Task 3: Delete `useApp.ts`
|
|
265
|
+
|
|
266
|
+
**Files:**
|
|
267
|
+
- Delete: `composables/useApp.ts`
|
|
268
|
+
|
|
269
|
+
- [ ] **Step 1: Delete the file**
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
git rm composables/useApp.ts
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- [ ] **Step 2: Commit**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
git commit -m "refactor(contexts): remove useApp.ts — functionality merged into useContext"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
### Task 4: `useAuth.js` — fix login body field
|
|
284
|
+
|
|
285
|
+
**Files:**
|
|
286
|
+
- Modify: `composables/useAuth.js`
|
|
287
|
+
|
|
288
|
+
The `performLogin` function currently sends `app: context` in the POST body. The backend (innertia-laravel) now expects the field to be named `context`.
|
|
289
|
+
|
|
290
|
+
- [ ] **Step 1: Fix the `performLogin` call**
|
|
291
|
+
|
|
292
|
+
Find line (~17):
|
|
293
|
+
```js
|
|
294
|
+
const data = await api.post(`${context}/auth/login`, { email, password, app: context })
|
|
295
|
+
```
|
|
296
|
+
Replace with:
|
|
297
|
+
```js
|
|
298
|
+
const data = await api.post(`${context}/auth/login`, { email, password, context })
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- [ ] **Step 2: Verify**
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
grep -n "app:" composables/useAuth.js
|
|
305
|
+
```
|
|
306
|
+
Expected: no output (zero matches — the old `app: context` field is gone).
|
|
307
|
+
|
|
308
|
+
- [ ] **Step 3: Commit**
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
git add composables/useAuth.js
|
|
312
|
+
git commit -m "fix(contexts): send context field instead of app in performLogin body"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Task 5: Rename and update middleware
|
|
318
|
+
|
|
319
|
+
**Files:**
|
|
320
|
+
- Delete: `middleware/03.apps.global.ts`
|
|
321
|
+
- Create: `middleware/03.contexts.global.ts`
|
|
322
|
+
|
|
323
|
+
- [ ] **Step 1: Create `middleware/03.contexts.global.ts` with updated content**
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
/**
|
|
327
|
+
* Middleware global que se ejecuta en cada navegación.
|
|
328
|
+
*
|
|
329
|
+
* Lee la declaración de contextos desde `appConfig.innertia.contexts`. Si el
|
|
330
|
+
* producto no declara contextos, este middleware no hace nada (feature inactivo).
|
|
331
|
+
*
|
|
332
|
+
* Cuando hay contextos declarados, resuelve qué contexto está intentando acceder
|
|
333
|
+
* el usuario según el prefijo de URL y:
|
|
334
|
+
* 1. Si la URL no cae en ningún contexto declarado → pasa (deja seguir)
|
|
335
|
+
* 2. Si la URL es la ruta de login del contexto → pasa (sin chequeo de permisos)
|
|
336
|
+
* 3. Si el usuario no está autenticado → deja que el middleware `auth` redirija
|
|
337
|
+
* 4. Si `availableContexts` está vacío (SSR sin hidratar) → intenta fetchMe()
|
|
338
|
+
* y si falla, pasa sin redirigir (mejor renderizar que redirigir mal)
|
|
339
|
+
* 5. Si el usuario está autenticado pero NO tiene acceso al contexto →
|
|
340
|
+
* redirige al primer contexto accesible (o a su loginPath si no tiene ninguno)
|
|
341
|
+
* 6. Si todo OK pero `currentContext !== targetContext.context` → sincroniza el
|
|
342
|
+
* contexto en el authStore y recarga permisos vía fetchMe()
|
|
343
|
+
*
|
|
344
|
+
* Esto es lo que permite el "cambio de contexto implícito por URL" sin botón.
|
|
345
|
+
*
|
|
346
|
+
* Numeración 03. para correr DESPUÉS de los middlewares saas
|
|
347
|
+
* (01.detect-subdomain, 02.validate-tenant).
|
|
348
|
+
*/
|
|
349
|
+
import type { ContextDefinition } from '../app.config'
|
|
350
|
+
|
|
351
|
+
export default defineNuxtRouteMiddleware(async (to) => {
|
|
352
|
+
const appConfig = useAppConfig()
|
|
353
|
+
const contexts = (appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
|
|
354
|
+
const list = Object.values(contexts)
|
|
355
|
+
|
|
356
|
+
// Si el producto no declaró contextos, el feature está inactivo
|
|
357
|
+
if (list.length === 0) return
|
|
358
|
+
|
|
359
|
+
// ¿La ruta cae bajo algún contexto?
|
|
360
|
+
const targetContext = list.find(ctx =>
|
|
361
|
+
to.path === ctx.path || to.path.startsWith(ctx.path + '/')
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if (!targetContext) return // ruta pública / fuera de contextos
|
|
365
|
+
|
|
366
|
+
// La ruta de login del propio contexto no requiere permisos
|
|
367
|
+
if (to.path === targetContext.loginPath) return
|
|
368
|
+
|
|
369
|
+
const authStore = useAuthStore()
|
|
370
|
+
|
|
371
|
+
// Si no está autenticado, deja que el middleware `auth` de la página haga el redirect
|
|
372
|
+
if (!authStore.isAuthenticated()) return
|
|
373
|
+
|
|
374
|
+
// ── Hidratación defensiva ─────────────────────────────────────────────────
|
|
375
|
+
// `availableContexts` se persiste en localStorage, que NO se lee en SSR.
|
|
376
|
+
// El plugin auth-init hace fetchMe en SSR pero puede fallar silenciosamente
|
|
377
|
+
// (ej. tenant no resuelto). Si llegamos con token válido pero array vacío,
|
|
378
|
+
// intentamos cargar los contextos antes de decidir el redirect.
|
|
379
|
+
if ((authStore.availableContexts ?? []).length === 0) {
|
|
380
|
+
try {
|
|
381
|
+
const { fetchMe } = useAuth()
|
|
382
|
+
await fetchMe()
|
|
383
|
+
} catch {
|
|
384
|
+
// no podemos resolver — pasamos: mejor renderizar la página que redirigir mal
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const userContexts = (authStore.availableContexts ?? []) as string[]
|
|
390
|
+
|
|
391
|
+
// Si tras hidratar seguimos sin contextos, NO redirigimos.
|
|
392
|
+
if (userContexts.length === 0) return
|
|
393
|
+
|
|
394
|
+
// ¿Tiene acceso al contexto?
|
|
395
|
+
if (!userContexts.includes(targetContext.context)) {
|
|
396
|
+
const fallback = list.find(ctx => userContexts.includes(ctx.context))
|
|
397
|
+
if (fallback) {
|
|
398
|
+
return navigateTo(fallback.home, { replace: true })
|
|
399
|
+
}
|
|
400
|
+
return navigateTo(list[0]?.loginPath ?? '/', { replace: true })
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Sincronizar currentContext silenciosamente si cambia
|
|
404
|
+
if (authStore.currentContext !== targetContext.context) {
|
|
405
|
+
authStore.setCurrentContext(targetContext.context)
|
|
406
|
+
try {
|
|
407
|
+
const { fetchMe } = useAuth()
|
|
408
|
+
await fetchMe()
|
|
409
|
+
} catch {
|
|
410
|
+
// best-effort
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- [ ] **Step 2: Remove the old middleware file**
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
git rm middleware/03.apps.global.ts
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- [ ] **Step 3: Stage and commit**
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
git add middleware/03.contexts.global.ts
|
|
426
|
+
git commit -m "refactor(contexts): rename 03.apps.global→03.contexts.global, innertia.apps→innertia.contexts, AppDefinition→ContextDefinition"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### Task 6: `useOrganization.ts` + `useMobileGuard.ts` — swap `useApp()` for `useContext()`
|
|
432
|
+
|
|
433
|
+
**Files:**
|
|
434
|
+
- Modify: `composables/useOrganization.ts`
|
|
435
|
+
- Modify: `composables/useMobileGuard.ts`
|
|
436
|
+
|
|
437
|
+
- [ ] **Step 1: Update `useOrganization.ts` — change `useApp()` to `useContext()`**
|
|
438
|
+
|
|
439
|
+
Find line 13:
|
|
440
|
+
```ts
|
|
441
|
+
const { current: currentApp } = useApp()
|
|
442
|
+
```
|
|
443
|
+
Replace with:
|
|
444
|
+
```ts
|
|
445
|
+
const { current: currentApp } = useContext()
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
(The local alias `currentApp` is kept — it's used on line 23 as `currentApp.value?.context`. No other changes needed in this file.)
|
|
449
|
+
|
|
450
|
+
- [ ] **Step 2: Verify `useOrganization.ts` — no remaining `useApp` references**
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
grep -n "useApp" composables/useOrganization.ts
|
|
454
|
+
```
|
|
455
|
+
Expected: no output.
|
|
456
|
+
|
|
457
|
+
- [ ] **Step 3: Update `useMobileGuard.ts` — full replacement**
|
|
458
|
+
|
|
459
|
+
Replace the full file with:
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
import { useMediaQuery } from '@vueuse/core'
|
|
463
|
+
import type { ContextDefinition } from '../app.config'
|
|
464
|
+
|
|
465
|
+
const PICKER_CHOICE_COOKIE = 'innertia_mobile_context_choice'
|
|
466
|
+
const PICKER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 días
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Detección reactiva de viewport mobile + lógica de landing.
|
|
470
|
+
*
|
|
471
|
+
* Reglas para `landing` (qué mostrar cuando el usuario entra a `/` en mobile):
|
|
472
|
+
* - 0 contextos mobile-friendly → blocker "abre en escritorio"
|
|
473
|
+
* - 1 contexto mobile-friendly → redirect directo al login de ese contexto
|
|
474
|
+
* - 2+ contextos mobile-friendly → picker (con cookie para recordar última elección)
|
|
475
|
+
*
|
|
476
|
+
* El breakpoint se lee desde `appConfig.innertia.mobile.breakpoint` (default 1024).
|
|
477
|
+
*/
|
|
478
|
+
export function useMobileGuard() {
|
|
479
|
+
const { all, current, accessible } = useContext()
|
|
480
|
+
const authStore = useAuthStore()
|
|
481
|
+
const appConfig = useAppConfig()
|
|
482
|
+
|
|
483
|
+
const breakpoint: number = appConfig.innertia?.mobile?.breakpoint ?? 1024
|
|
484
|
+
const isMobile = useMediaQuery(`(max-width: ${breakpoint - 1}px)`)
|
|
485
|
+
|
|
486
|
+
/** Contextos con mode 'allow' en mobile. */
|
|
487
|
+
const mobileApps = computed<ContextDefinition[]>(() =>
|
|
488
|
+
all.value.filter(a => a.mobile?.mode === 'allow')
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
/** Contextos mobile-friendly + accesibles para el usuario autenticado. */
|
|
492
|
+
const mobileAccessibleApps = computed<ContextDefinition[]>(() =>
|
|
493
|
+
accessible.value.filter(a => a.mobile?.mode === 'allow')
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
/** ¿El contexto actual está bloqueado en mobile? */
|
|
497
|
+
const isCurrentAppBlocked = computed<boolean>(() => {
|
|
498
|
+
if (!isMobile.value) return false
|
|
499
|
+
if (!current.value) return false
|
|
500
|
+
return current.value.mobile?.mode === 'block'
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Para el blocker: si el usuario está autenticado y tiene OTRO contexto mobile-friendly,
|
|
505
|
+
* lo ofrecemos como fallback ("continuar en X").
|
|
506
|
+
*/
|
|
507
|
+
const mobileFallbackApp = computed<ContextDefinition | null>(() => {
|
|
508
|
+
if (!authStore.isAuthenticated()) return null
|
|
509
|
+
return mobileAccessibleApps.value[0] ?? null
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
// ── Cookie helpers para recordar última elección del picker ─────────────────
|
|
513
|
+
function rememberPickerChoice(contextKey: string) {
|
|
514
|
+
if (!(appConfig.innertia?.mobile?.rememberChoice ?? true)) return
|
|
515
|
+
const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE, {
|
|
516
|
+
maxAge: PICKER_COOKIE_MAX_AGE,
|
|
517
|
+
sameSite: 'lax',
|
|
518
|
+
})
|
|
519
|
+
cookie.value = contextKey
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function getRememberedPickerChoice(): string | null {
|
|
523
|
+
const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE)
|
|
524
|
+
return cookie.value ?? null
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function clearRememberedPickerChoice() {
|
|
528
|
+
const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE)
|
|
529
|
+
cookie.value = null
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
isMobile,
|
|
534
|
+
mobileApps,
|
|
535
|
+
mobileAccessibleApps,
|
|
536
|
+
isCurrentAppBlocked,
|
|
537
|
+
mobileFallbackApp,
|
|
538
|
+
rememberPickerChoice,
|
|
539
|
+
getRememberedPickerChoice,
|
|
540
|
+
clearRememberedPickerChoice,
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
- [ ] **Step 4: Verify no remaining `useApp` or `AppDefinition` references in both files**
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
grep -n "useApp\|AppDefinition\|AppMobileMode" composables/useOrganization.ts composables/useMobileGuard.ts
|
|
549
|
+
```
|
|
550
|
+
Expected: no output.
|
|
551
|
+
|
|
552
|
+
- [ ] **Step 5: Commit**
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
git add composables/useOrganization.ts composables/useMobileGuard.ts
|
|
556
|
+
git commit -m "refactor(contexts): useOrganization + useMobileGuard — useApp()→useContext(), AppDefinition→ContextDefinition"
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
### Task 7: Docs — CHANGELOG + final verification
|
|
562
|
+
|
|
563
|
+
**Files:**
|
|
564
|
+
- Create: `CHANGELOG.md`
|
|
565
|
+
|
|
566
|
+
- [ ] **Step 1: Create `CHANGELOG.md`**
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
cat > CHANGELOG.md << 'EOF'
|
|
570
|
+
# Changelog
|
|
571
|
+
|
|
572
|
+
All notable changes to `@innertia-solutions/innertia-nuxt` are documented here.
|
|
573
|
+
|
|
574
|
+
## [Unreleased]
|
|
575
|
+
|
|
576
|
+
### BREAKING CHANGES
|
|
577
|
+
|
|
578
|
+
#### apps → contexts rename
|
|
579
|
+
|
|
580
|
+
Aligns frontend with innertia-laravel which already renamed the concept.
|
|
581
|
+
|
|
582
|
+
**Config (`app.config.ts` / `nuxt.config.ts` in your product):**
|
|
583
|
+
```diff
|
|
584
|
+
- innertia.apps: { backoffice: { ... }, technician: { ... } }
|
|
585
|
+
+ innertia.contexts: { backoffice: { ... }, technician: { ... } }
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Types:**
|
|
589
|
+
```diff
|
|
590
|
+
- import type { AppDefinition } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
591
|
+
+ import type { ContextDefinition } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
592
|
+
|
|
593
|
+
- import type { AppMobileMode } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
594
|
+
+ import type { ContextMobileMode } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**Composables:**
|
|
598
|
+
```diff
|
|
599
|
+
- const { current, all, accessible, canAccess } = useApp()
|
|
600
|
+
+ const { current, all, accessible, canAccess } = useContext()
|
|
601
|
+
# useContext() now also returns: contexts (dict), currentContext, availableContexts,
|
|
602
|
+
# switchContext(), confirmSwitch(), hasAccessToContext()
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
`useApp()` has been removed. Use `useContext()` for all context/app logic.
|
|
606
|
+
|
|
607
|
+
**Login body field:**
|
|
608
|
+
|
|
609
|
+
The `performLogin()` function in `useAuth` now sends `context` instead of `app` in
|
|
610
|
+
the POST body — matching what innertia-laravel now expects.
|
|
611
|
+
EOF
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
- [ ] **Step 2: Final grep — verify no stale references remain in the entire library**
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
grep -r "innertia\.apps\|AppDefinition\|AppMobileMode\|useApp()\|'app:'\|\"app:\"" \
|
|
618
|
+
--include="*.ts" --include="*.js" --include="*.vue" \
|
|
619
|
+
--exclude-dir=".nuxt" --exclude-dir="node_modules" \
|
|
620
|
+
.
|
|
621
|
+
```
|
|
622
|
+
Expected: **no output**. If any matches appear, fix them before committing.
|
|
623
|
+
|
|
624
|
+
- [ ] **Step 3: Commit**
|
|
625
|
+
|
|
626
|
+
```bash
|
|
627
|
+
git add CHANGELOG.md
|
|
628
|
+
git commit -m "docs(contexts): add CHANGELOG with apps→contexts BREAKING CHANGE"
|
|
629
|
+
```
|
|
630
|
+
EOF
|