@nocios/crudify-ui 1.0.83 → 1.0.85
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/README.md +302 -0
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -369,6 +369,308 @@ function AuthenticatedApp() {
|
|
|
369
369
|
export default AuthenticatedApp;
|
|
370
370
|
```
|
|
371
371
|
|
|
372
|
+
## 🏗️ Implementación con UnifiedProvider (Recomendado)
|
|
373
|
+
|
|
374
|
+
### Patrón Unificado para Apps Completas
|
|
375
|
+
|
|
376
|
+
Para aplicaciones que necesitan gestión completa de estado y autenticación, recomendamos usar el patrón UnifiedProvider que combina la gestión de configuración y autenticación en un solo componente:
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
// src/contexts/UnifiedProvider.tsx
|
|
380
|
+
import React, { createContext, useContext, useMemo, useState, useCallback, useEffect, type ReactNode } from "react"
|
|
381
|
+
import { createTheme } from "@mui/material"
|
|
382
|
+
import { crudify, type CrudifyEnvType } from "@nocios/crudify-ui"
|
|
383
|
+
|
|
384
|
+
interface AppContextValue {
|
|
385
|
+
logo: string | null
|
|
386
|
+
setLogo: (logo: string) => void
|
|
387
|
+
publicApiKey: string | null
|
|
388
|
+
setPublicApiKey: (key: string | null) => void
|
|
389
|
+
env: CrudifyEnvType
|
|
390
|
+
setEnv: (env: CrudifyEnvType) => void
|
|
391
|
+
appName: string | null
|
|
392
|
+
setAppName: (name: string | null) => void
|
|
393
|
+
colors: { primaryColor: string; [key: string]: string }
|
|
394
|
+
setColors: (colors: any) => void
|
|
395
|
+
theme: any
|
|
396
|
+
setTheme: (theme: any) => void
|
|
397
|
+
configLoading: boolean
|
|
398
|
+
configError: string | null
|
|
399
|
+
loginActions: string[]
|
|
400
|
+
setLoginActions: (actions: string[]) => void
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
interface DataContextValue {
|
|
404
|
+
token: string | null
|
|
405
|
+
setToken: (token: string | null) => void
|
|
406
|
+
user: Record<string, any> | null
|
|
407
|
+
isInitialized: boolean
|
|
408
|
+
initializationError: string | null
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const AppContext = createContext<AppContextValue | null>(null)
|
|
412
|
+
const DataContext = createContext<DataContextValue | null>(null)
|
|
413
|
+
|
|
414
|
+
const UnifiedProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
415
|
+
// Estado para token y usuario
|
|
416
|
+
const [token, setTokenState] = useState<string | null>(null)
|
|
417
|
+
const [user, setUser] = useState<Record<string, any> | null>(null)
|
|
418
|
+
const [isFullyInitialized, setIsFullyInitialized] = useState(false)
|
|
419
|
+
|
|
420
|
+
// Configuración desde variables de entorno
|
|
421
|
+
const config = useMemo(() => ({
|
|
422
|
+
publicApiKey: import.meta.env.VITE_TEST_PUBLIC_API_KEY || 'default-key',
|
|
423
|
+
env: import.meta.env.VITE_TEST_ENV || 'prod',
|
|
424
|
+
appName: 'Mi Aplicación'
|
|
425
|
+
}), [])
|
|
426
|
+
|
|
427
|
+
const handleSetToken = useCallback((newToken: string | null) => {
|
|
428
|
+
setTokenState(newToken)
|
|
429
|
+
if (newToken) {
|
|
430
|
+
sessionStorage.setItem('crud_token', newToken)
|
|
431
|
+
} else {
|
|
432
|
+
sessionStorage.removeItem('crud_token')
|
|
433
|
+
}
|
|
434
|
+
}, [])
|
|
435
|
+
|
|
436
|
+
// Inicializar token desde sessionStorage
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
const savedToken = sessionStorage.getItem('crud_token')
|
|
439
|
+
if (savedToken) {
|
|
440
|
+
setTokenState(savedToken)
|
|
441
|
+
}
|
|
442
|
+
}, [])
|
|
443
|
+
|
|
444
|
+
// Inicialización de Crudify cuando hay token
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
if (!token) {
|
|
447
|
+
setIsFullyInitialized(false)
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const initializeCrudify = async () => {
|
|
452
|
+
if (isFullyInitialized) return
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
await crudify.config(config.env)
|
|
456
|
+
await crudify.init(config.publicApiKey, "none")
|
|
457
|
+
crudify.setToken(token)
|
|
458
|
+
setIsFullyInitialized(true)
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error('Error initializing Crudify:', error)
|
|
461
|
+
setIsFullyInitialized(true) // Evitar bucles
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
initializeCrudify()
|
|
466
|
+
}, [token, config.env, config.publicApiKey, isFullyInitialized])
|
|
467
|
+
|
|
468
|
+
// Contexto de App
|
|
469
|
+
const appContextValue: AppContextValue = {
|
|
470
|
+
logo: null,
|
|
471
|
+
setLogo: () => {},
|
|
472
|
+
publicApiKey: config.publicApiKey,
|
|
473
|
+
setPublicApiKey: () => {},
|
|
474
|
+
env: config.env as CrudifyEnvType,
|
|
475
|
+
setEnv: () => {},
|
|
476
|
+
appName: config.appName,
|
|
477
|
+
setAppName: () => {},
|
|
478
|
+
colors: { primaryColor: "#1066BA" },
|
|
479
|
+
setColors: () => {},
|
|
480
|
+
theme: createTheme({ palette: { primary: { main: "#1066BA" } } }),
|
|
481
|
+
setTheme: () => {},
|
|
482
|
+
configLoading: false,
|
|
483
|
+
configError: null,
|
|
484
|
+
loginActions: [],
|
|
485
|
+
setLoginActions: () => {}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Contexto de Data
|
|
489
|
+
const dataContextValue: DataContextValue = {
|
|
490
|
+
token,
|
|
491
|
+
setToken: handleSetToken,
|
|
492
|
+
user,
|
|
493
|
+
isInitialized: !!token && isFullyInitialized,
|
|
494
|
+
initializationError: null
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<AppContext.Provider value={appContextValue}>
|
|
499
|
+
<DataContext.Provider value={dataContextValue}>
|
|
500
|
+
{children}
|
|
501
|
+
</DataContext.Provider>
|
|
502
|
+
</AppContext.Provider>
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export const useApp = (): AppContextValue => {
|
|
507
|
+
const context = useContext(AppContext)
|
|
508
|
+
if (!context) throw new Error("useApp should be used within a UnifiedProvider")
|
|
509
|
+
return context
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export const useData = (): DataContextValue => {
|
|
513
|
+
const context = useContext(DataContext)
|
|
514
|
+
if (!context) throw new Error("useData should be used within a UnifiedProvider")
|
|
515
|
+
return context
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export { UnifiedProvider }
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Implementación del AppStatusHandler
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
// src/components/AppStatusHandler/index.tsx
|
|
525
|
+
import { type ReactNode } from "react"
|
|
526
|
+
import { useLocation } from "react-router-dom"
|
|
527
|
+
import { useApp, useData } from "@contexts/UnifiedProvider"
|
|
528
|
+
import LoadingApp from "@components/LoadingApp"
|
|
529
|
+
import { CssBaseline, ThemeProvider } from "@mui/material"
|
|
530
|
+
import InitError from "@components/InitError"
|
|
531
|
+
|
|
532
|
+
interface AppStatusHandlerProps {
|
|
533
|
+
children: ReactNode
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const AppStatusHandler = ({ children }: AppStatusHandlerProps) => {
|
|
537
|
+
const location = useLocation()
|
|
538
|
+
const { theme, configLoading, configError } = useApp()
|
|
539
|
+
const { isInitialized, initializationError } = useData()
|
|
540
|
+
|
|
541
|
+
// Rutas que no requieren inicialización completa
|
|
542
|
+
const authRoutes = ['/login', '/login/forgotPassword', '/login/checkCode', '/login/resetPassword']
|
|
543
|
+
const isAuthRoute = authRoutes.some(route => location.pathname.startsWith(route))
|
|
544
|
+
|
|
545
|
+
if (configLoading) {
|
|
546
|
+
return <LoadingApp stage="config" />
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!theme) {
|
|
550
|
+
return <LoadingApp stage="theme" />
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Solo requerir inicialización para rutas protegidas
|
|
554
|
+
if (!isInitialized && !isAuthRoute) {
|
|
555
|
+
return <LoadingApp stage="initializing" />
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (configError || initializationError) {
|
|
559
|
+
return <InitError configError={configError} initializationError={Boolean(initializationError)} />
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
<ThemeProvider theme={theme}>
|
|
564
|
+
<CssBaseline />
|
|
565
|
+
{children}
|
|
566
|
+
</ThemeProvider>
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export default AppStatusHandler
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Implementación de ProtectedRoute
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
576
|
+
// src/components/Auth/ProtectedRoute/index.tsx
|
|
577
|
+
import { type ReactNode } from "react"
|
|
578
|
+
import { Navigate, useLocation } from "react-router-dom"
|
|
579
|
+
import { useData } from "@contexts/UnifiedProvider"
|
|
580
|
+
|
|
581
|
+
interface ProtectedRouteProps {
|
|
582
|
+
children: ReactNode
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
|
586
|
+
const { token } = useData()
|
|
587
|
+
const location = useLocation()
|
|
588
|
+
|
|
589
|
+
if (!token) {
|
|
590
|
+
const fullPath = location.pathname + location.search
|
|
591
|
+
const redirectPath = encodeURIComponent(fullPath)
|
|
592
|
+
return <Navigate to={`/login?redirect=${redirectPath}`} replace />
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return children
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export default ProtectedRoute
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Configuración de la App Principal
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
// src/main.tsx
|
|
605
|
+
import React from 'react'
|
|
606
|
+
import ReactDOM from 'react-dom/client'
|
|
607
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
608
|
+
import { UnifiedProvider } from '@contexts/UnifiedProvider'
|
|
609
|
+
import { GlobalNotificationProvider } from '@contexts/GlobalNotificationProvider'
|
|
610
|
+
import AppStatusHandler from '@components/AppStatusHandler'
|
|
611
|
+
import App from './App'
|
|
612
|
+
|
|
613
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
614
|
+
<React.StrictMode>
|
|
615
|
+
<BrowserRouter>
|
|
616
|
+
<UnifiedProvider>
|
|
617
|
+
<GlobalNotificationProvider>
|
|
618
|
+
<AppStatusHandler>
|
|
619
|
+
<App />
|
|
620
|
+
</AppStatusHandler>
|
|
621
|
+
</GlobalNotificationProvider>
|
|
622
|
+
</UnifiedProvider>
|
|
623
|
+
</BrowserRouter>
|
|
624
|
+
</React.StrictMode>,
|
|
625
|
+
)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Uso en Componentes
|
|
629
|
+
|
|
630
|
+
```tsx
|
|
631
|
+
// Cualquier componente de la app
|
|
632
|
+
import { useApp, useData } from '@contexts/UnifiedProvider'
|
|
633
|
+
|
|
634
|
+
function Dashboard() {
|
|
635
|
+
const { appName, theme } = useApp()
|
|
636
|
+
const { user, token, isInitialized } = useData()
|
|
637
|
+
|
|
638
|
+
if (!isInitialized) {
|
|
639
|
+
return <div>Inicializando...</div>
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<div>
|
|
644
|
+
<h1>Bienvenido a {appName}</h1>
|
|
645
|
+
<p>Usuario: {user?.email}</p>
|
|
646
|
+
<p>Token válido: {token ? 'Sí' : 'No'}</p>
|
|
647
|
+
</div>
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Variables de Entorno Requeridas
|
|
653
|
+
|
|
654
|
+
```env
|
|
655
|
+
# .env.development
|
|
656
|
+
VITE_TEST_PUBLIC_API_KEY=tu-clave-de-api-de-desarrollo
|
|
657
|
+
VITE_TEST_ENV=dev
|
|
658
|
+
|
|
659
|
+
# .env.production
|
|
660
|
+
VITE_TEST_PUBLIC_API_KEY=tu-clave-de-api-de-produccion
|
|
661
|
+
VITE_TEST_ENV=prod
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Ventajas de este Patrón
|
|
665
|
+
|
|
666
|
+
✅ **Inicialización automática**: Crudify se inicializa automáticamente cuando hay token
|
|
667
|
+
✅ **Gestión unificada**: Un solo provider para configuración y autenticación
|
|
668
|
+
✅ **Rutas protegidas**: Manejo automático de redirecciones según autenticación
|
|
669
|
+
✅ **Estados de carga**: Diferentes estados para config, theme e inicialización
|
|
670
|
+
✅ **Variables de entorno**: Configuración centralizada desde .env
|
|
671
|
+
✅ **Compatibilidad completa**: Mantiene la API de providers anteriores
|
|
672
|
+
✅ **Sin recursión**: Evita problemas de bucles infinitos en la inicialización
|
|
673
|
+
|
|
372
674
|
---
|
|
373
675
|
|
|
374
676
|
**Version:** 1.0.0+
|
package/dist/index.js
CHANGED
|
@@ -2674,7 +2674,7 @@ var _TokenManager = class _TokenManager {
|
|
|
2674
2674
|
* Parse the current JWT token
|
|
2675
2675
|
*/
|
|
2676
2676
|
parseToken(token) {
|
|
2677
|
-
const targetToken = token || this.
|
|
2677
|
+
const targetToken = token || this.tokenCache;
|
|
2678
2678
|
if (!targetToken) {
|
|
2679
2679
|
return null;
|
|
2680
2680
|
}
|
|
@@ -2691,7 +2691,7 @@ var _TokenManager = class _TokenManager {
|
|
|
2691
2691
|
* Check if a token is valid (properly formatted and not expired)
|
|
2692
2692
|
*/
|
|
2693
2693
|
isTokenValid(token) {
|
|
2694
|
-
const targetToken = token || this.
|
|
2694
|
+
const targetToken = token || this.tokenCache;
|
|
2695
2695
|
if (!targetToken) {
|
|
2696
2696
|
return false;
|
|
2697
2697
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -2639,7 +2639,7 @@ var _TokenManager = class _TokenManager {
|
|
|
2639
2639
|
* Parse the current JWT token
|
|
2640
2640
|
*/
|
|
2641
2641
|
parseToken(token) {
|
|
2642
|
-
const targetToken = token || this.
|
|
2642
|
+
const targetToken = token || this.tokenCache;
|
|
2643
2643
|
if (!targetToken) {
|
|
2644
2644
|
return null;
|
|
2645
2645
|
}
|
|
@@ -2656,7 +2656,7 @@ var _TokenManager = class _TokenManager {
|
|
|
2656
2656
|
* Check if a token is valid (properly formatted and not expired)
|
|
2657
2657
|
*/
|
|
2658
2658
|
isTokenValid(token) {
|
|
2659
|
-
const targetToken = token || this.
|
|
2659
|
+
const targetToken = token || this.tokenCache;
|
|
2660
2660
|
if (!targetToken) {
|
|
2661
2661
|
return false;
|
|
2662
2662
|
}
|