@asteby/metacore-runtime-react 23.10.0 → 23.11.0
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 +12 -0
- package/README.md +1 -0
- package/dist/license/license-gate.d.ts +8 -1
- package/dist/license/license-gate.d.ts.map +1 -1
- package/dist/license/license-gate.js +8 -5
- package/package.json +1 -1
- package/src/__tests__/license-gate.test.tsx +40 -0
- package/src/license/license-gate.tsx +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 023dc28: feat(license-gate): gateo por rol del formulario de activación.
|
|
8
|
+
|
|
9
|
+
`<LicenseGate>` acepta dos props nuevas (backward-compatible):
|
|
10
|
+
- `canActivate?: boolean` (default `true`): cuando es `false`, el modal bloqueante se muestra SIN el formulario de activación.
|
|
11
|
+
- `readOnlyMessage?: string`: mensaje mostrado en ese caso (default: "Contacta al administrador de la plataforma para activar la licencia.").
|
|
12
|
+
|
|
13
|
+
Aplica a todas las variantes del modal (missing/invalid/expired, incluido el copy de trial vencido). Permite que solo el Platform Root/superadmin active la licencia mientras el resto de usuarios ve el bloqueo con la indicación de a quién contactar.
|
|
14
|
+
|
|
3
15
|
## 23.10.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ export function Shell({ children }) {
|
|
|
73
73
|
- Enforcement && `missing`/`invalid`/`expired` → modal **bloqueante** con el formulario de activación.
|
|
74
74
|
`plan === 'trial'` + `expired` → copy *"Tu prueba gratuita terminó"*.
|
|
75
75
|
- Activación exitosa desbloquea **sin recargar**: el host refresca el `state` y el gate se desmonta.
|
|
76
|
+
- **Gateo por rol** con `canActivate` (default `true`): cuando es `false`, el modal bloqueante se muestra **sin formulario** de activación y en su lugar aparece `readOnlyMessage` (default: *"Contacta al administrador de la plataforma para activar la licencia."*). Así solo el Platform Root/superadmin activa; los demás usuarios ven el bloqueo con la indicación de a quién contactar.
|
|
76
77
|
|
|
77
78
|
## Minimal usage
|
|
78
79
|
|
|
@@ -34,7 +34,14 @@ export interface LicenseGateProps {
|
|
|
34
34
|
branding?: LicenseBranding;
|
|
35
35
|
/** Acción "Gestionar licencia" del banner degradado (opcional). */
|
|
36
36
|
onManage?: () => void;
|
|
37
|
+
/** Si el usuario actual puede activar la licencia (p. ej. Platform Root /
|
|
38
|
+
* superadmin). Default true — backward-compatible. Cuando es false, el modal
|
|
39
|
+
* bloqueante se muestra SIN el formulario de activación y en su lugar
|
|
40
|
+
* aparece `readOnlyMessage`. */
|
|
41
|
+
canActivate?: boolean;
|
|
42
|
+
/** Mensaje mostrado en el modal bloqueante cuando `canActivate` es false. */
|
|
43
|
+
readOnlyMessage?: string;
|
|
37
44
|
className?: string;
|
|
38
45
|
}
|
|
39
|
-
export declare function LicenseGate({ state, onActivate, children, branding, onManage, className, }: LicenseGateProps): import("react").JSX.Element;
|
|
46
|
+
export declare function LicenseGate({ state, onActivate, children, branding, onManage, canActivate, readOnlyMessage, className, }: LicenseGateProps): import("react").JSX.Element;
|
|
40
47
|
//# sourceMappingURL=license-gate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"license-gate.d.ts","sourceRoot":"","sources":["../../src/license/license-gate.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAIhD,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,eAAe,EACvB,MAAM,SAAS,CAAA;AAQhB,MAAM,WAAW,gBAAgB;IAC7B;4EACwE;IACxE,KAAK,EAAE,YAAY,GAAG,SAAS,CAAA;IAC/B;;gCAE4B;IAC5B,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,QAAQ,EAAE,SAAS,CAAA;IACnB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,+
|
|
1
|
+
{"version":3,"file":"license-gate.d.ts","sourceRoot":"","sources":["../../src/license/license-gate.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAIhD,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,eAAe,EACvB,MAAM,SAAS,CAAA;AAQhB,MAAM,WAAW,gBAAgB;IAC7B;4EACwE;IACxE,KAAK,EAAE,YAAY,GAAG,SAAS,CAAA;IAC/B;;gCAE4B;IAC5B,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,QAAQ,EAAE,SAAS,CAAA;IACnB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;oCAGgC;IAChC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,WAAkB,EAClB,eAAe,EACf,SAAS,GACZ,EAAE,gBAAgB,+BAuBlB"}
|
|
@@ -30,11 +30,11 @@ import { LicenseExpiryBanner } from './license-expiry-banner';
|
|
|
30
30
|
function cx(...parts) {
|
|
31
31
|
return parts.filter(Boolean).join(' ');
|
|
32
32
|
}
|
|
33
|
-
export function LicenseGate({ state, onActivate, children, branding, onManage, className, }) {
|
|
33
|
+
export function LicenseGate({ state, onActivate, children, branding, onManage, canActivate = true, readOnlyMessage, className, }) {
|
|
34
34
|
const blocking = isLicenseBlocking(state);
|
|
35
|
-
return (_jsxs(_Fragment, { children: [!blocking && (_jsx(LicenseExpiryBanner, { state: state, onManage: onManage })), children, blocking && state && (_jsx(LicenseGateModal, { state: state, onActivate: onActivate, branding: branding, className: className }))] }));
|
|
35
|
+
return (_jsxs(_Fragment, { children: [!blocking && (_jsx(LicenseExpiryBanner, { state: state, onManage: onManage })), children, blocking && state && (_jsx(LicenseGateModal, { state: state, onActivate: onActivate, branding: branding, canActivate: canActivate, readOnlyMessage: readOnlyMessage, className: className }))] }));
|
|
36
36
|
}
|
|
37
|
-
function LicenseGateModal({ state, onActivate, branding, className, }) {
|
|
37
|
+
function LicenseGateModal({ state, onActivate, branding, canActivate, readOnlyMessage, className, }) {
|
|
38
38
|
const { t } = useTranslation();
|
|
39
39
|
const [code, setCode] = useState('');
|
|
40
40
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -87,7 +87,10 @@ function LicenseGateModal({ state, onActivate, branding, className, }) {
|
|
|
87
87
|
? branding.name
|
|
88
88
|
: t('license.gate.instance', {
|
|
89
89
|
defaultValue: 'Esta instancia',
|
|
90
|
-
}) })] })] })] }), _jsx("p", { className: "text-muted-foreground text-sm", children: description }), state.reason && (_jsx("p", { className: "bg-muted/50 text-muted-foreground rounded-md px-3 py-2 text-xs", children: state.reason })),
|
|
90
|
+
}) })] })] })] }), _jsx("p", { className: "text-muted-foreground text-sm", children: description }), state.reason && (_jsx("p", { className: "bg-muted/50 text-muted-foreground rounded-md px-3 py-2 text-xs", children: state.reason })), !canActivate ? (_jsx("p", { role: "note", className: "border-border bg-muted/40 text-muted-foreground rounded-md border px-3 py-3 text-sm", children: readOnlyMessage ??
|
|
91
|
+
t('license.gate.read_only', {
|
|
92
|
+
defaultValue: 'Contacta al administrador de la plataforma para activar la licencia.',
|
|
93
|
+
}) })) : (_jsxs("form", { className: "flex flex-col gap-2", onSubmit: (e) => {
|
|
91
94
|
e.preventDefault();
|
|
92
95
|
void submit();
|
|
93
96
|
}, children: [_jsx("label", { htmlFor: "license-gate-code", className: "text-sm font-medium", children: t('license.gate.code_label', {
|
|
@@ -104,5 +107,5 @@ function LicenseGateModal({ state, onActivate, branding, className, }) {
|
|
|
104
107
|
})
|
|
105
108
|
: t('license.gate.activate', {
|
|
106
109
|
defaultValue: 'Activar licencia',
|
|
107
|
-
})] })] })] }) }) }));
|
|
110
|
+
})] })] }))] }) }) }));
|
|
108
111
|
}
|
package/package.json
CHANGED
|
@@ -168,6 +168,46 @@ describe('LicenseGate', () => {
|
|
|
168
168
|
expect(btn.disabled).toBe(true)
|
|
169
169
|
})
|
|
170
170
|
|
|
171
|
+
it('sin canActivate oculta el form y muestra el mensaje read-only por defecto', () => {
|
|
172
|
+
render(
|
|
173
|
+
<LicenseGate state={make({ status: 'missing' })} onActivate={noop} canActivate={false}>
|
|
174
|
+
{CHILD}
|
|
175
|
+
</LicenseGate>,
|
|
176
|
+
)
|
|
177
|
+
expect(screen.getByRole('dialog')).toBeTruthy()
|
|
178
|
+
expect(screen.queryByLabelText('Clave o token de licencia')).toBeNull()
|
|
179
|
+
expect(screen.queryByText('Activar licencia')).toBeNull()
|
|
180
|
+
expect(
|
|
181
|
+
screen.getByText('Contacta al administrador de la plataforma para activar la licencia.'),
|
|
182
|
+
).toBeTruthy()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('sin canActivate respeta el readOnlyMessage custom, incl. trial vencido', () => {
|
|
186
|
+
render(
|
|
187
|
+
<LicenseGate
|
|
188
|
+
state={make({ status: 'expired', plan: 'trial' })}
|
|
189
|
+
onActivate={noop}
|
|
190
|
+
canActivate={false}
|
|
191
|
+
readOnlyMessage="Pídele a Ana que active la licencia."
|
|
192
|
+
>
|
|
193
|
+
{CHILD}
|
|
194
|
+
</LicenseGate>,
|
|
195
|
+
)
|
|
196
|
+
expect(screen.getByText('Tu prueba gratuita terminó')).toBeTruthy()
|
|
197
|
+
expect(screen.queryByLabelText('Clave o token de licencia')).toBeNull()
|
|
198
|
+
expect(screen.getByText('Pídele a Ana que active la licencia.')).toBeTruthy()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('canActivate true (default) mantiene el form de activación', () => {
|
|
202
|
+
render(
|
|
203
|
+
<LicenseGate state={make({ status: 'invalid' })} onActivate={noop}>
|
|
204
|
+
{CHILD}
|
|
205
|
+
</LicenseGate>,
|
|
206
|
+
)
|
|
207
|
+
expect(screen.getByLabelText('Clave o token de licencia')).toBeTruthy()
|
|
208
|
+
expect(screen.getByText('Activar licencia')).toBeTruthy()
|
|
209
|
+
})
|
|
210
|
+
|
|
171
211
|
it('grace degrada con banner en vez de bloquear', () => {
|
|
172
212
|
render(
|
|
173
213
|
<LicenseGate
|
|
@@ -49,6 +49,13 @@ export interface LicenseGateProps {
|
|
|
49
49
|
branding?: LicenseBranding
|
|
50
50
|
/** Acción "Gestionar licencia" del banner degradado (opcional). */
|
|
51
51
|
onManage?: () => void
|
|
52
|
+
/** Si el usuario actual puede activar la licencia (p. ej. Platform Root /
|
|
53
|
+
* superadmin). Default true — backward-compatible. Cuando es false, el modal
|
|
54
|
+
* bloqueante se muestra SIN el formulario de activación y en su lugar
|
|
55
|
+
* aparece `readOnlyMessage`. */
|
|
56
|
+
canActivate?: boolean
|
|
57
|
+
/** Mensaje mostrado en el modal bloqueante cuando `canActivate` es false. */
|
|
58
|
+
readOnlyMessage?: string
|
|
52
59
|
className?: string
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -58,6 +65,8 @@ export function LicenseGate({
|
|
|
58
65
|
children,
|
|
59
66
|
branding,
|
|
60
67
|
onManage,
|
|
68
|
+
canActivate = true,
|
|
69
|
+
readOnlyMessage,
|
|
61
70
|
className,
|
|
62
71
|
}: LicenseGateProps) {
|
|
63
72
|
const blocking = isLicenseBlocking(state)
|
|
@@ -75,6 +84,8 @@ export function LicenseGate({
|
|
|
75
84
|
state={state}
|
|
76
85
|
onActivate={onActivate}
|
|
77
86
|
branding={branding}
|
|
87
|
+
canActivate={canActivate}
|
|
88
|
+
readOnlyMessage={readOnlyMessage}
|
|
78
89
|
className={className}
|
|
79
90
|
/>
|
|
80
91
|
)}
|
|
@@ -86,6 +97,8 @@ interface LicenseGateModalProps {
|
|
|
86
97
|
state: LicenseState
|
|
87
98
|
onActivate: (code: string) => Promise<void>
|
|
88
99
|
branding?: LicenseBranding
|
|
100
|
+
canActivate: boolean
|
|
101
|
+
readOnlyMessage?: string
|
|
89
102
|
className?: string
|
|
90
103
|
}
|
|
91
104
|
|
|
@@ -93,6 +106,8 @@ function LicenseGateModal({
|
|
|
93
106
|
state,
|
|
94
107
|
onActivate,
|
|
95
108
|
branding,
|
|
109
|
+
canActivate,
|
|
110
|
+
readOnlyMessage,
|
|
96
111
|
className,
|
|
97
112
|
}: LicenseGateModalProps) {
|
|
98
113
|
const { t } = useTranslation()
|
|
@@ -214,6 +229,18 @@ function LicenseGateModal({
|
|
|
214
229
|
</p>
|
|
215
230
|
)}
|
|
216
231
|
|
|
232
|
+
{!canActivate ? (
|
|
233
|
+
<p
|
|
234
|
+
role="note"
|
|
235
|
+
className="border-border bg-muted/40 text-muted-foreground rounded-md border px-3 py-3 text-sm"
|
|
236
|
+
>
|
|
237
|
+
{readOnlyMessage ??
|
|
238
|
+
t('license.gate.read_only', {
|
|
239
|
+
defaultValue:
|
|
240
|
+
'Contacta al administrador de la plataforma para activar la licencia.',
|
|
241
|
+
})}
|
|
242
|
+
</p>
|
|
243
|
+
) : (
|
|
217
244
|
<form
|
|
218
245
|
className="flex flex-col gap-2"
|
|
219
246
|
onSubmit={(e) => {
|
|
@@ -270,6 +297,7 @@ function LicenseGateModal({
|
|
|
270
297
|
})}
|
|
271
298
|
</Button>
|
|
272
299
|
</form>
|
|
300
|
+
)}
|
|
273
301
|
</div>
|
|
274
302
|
</div>
|
|
275
303
|
</div>
|