fix(plugins): preserve disabled state, keep sidebar accessible, load inactive translations

main
Juan Manuel Bardallo 2026-10-26 13:32:23 +01:00
parent 2aea3beab8
commit 80e8cf95e6
5 changed files with 97 additions and 13 deletions

View File

@ -443,6 +443,61 @@
height: 48px;
}
.plugin-disabled-card {
display: flex;
align-items: center;
gap: 24px;
background: #ffffff;
border: 1px dashed #cbd5e1;
border-radius: 12px;
padding: 36px 30px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
}
.plugin-disabled-icon-box {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
background: #f1f5f9;
color: #64748b;
border-radius: 12px;
flex-shrink: 0;
}
.plugin-disabled-icon {
font-size: 32px;
width: 32px;
height: 32px;
}
.plugin-disabled-content {
display: flex;
flex-direction: column;
gap: 6px;
}
.plugin-disabled-title {
margin: 0;
font-size: 1.15rem;
font-weight: 700;
color: #334155;
}
.plugin-disabled-text {
margin: 0;
color: #64748b;
font-size: 0.92rem;
line-height: 1.5;
}
.plugin-disabled-hint {
margin: 4px 0 0 0;
color: #94a3b8;
font-size: 0.85rem;
}
@media (max-width: 900px) {
.section-header-row {
flex-direction: column;

View File

@ -23,7 +23,8 @@
</div>
</div>
<div class="settings-form-wrapper">
@if (plugin.enabled) {
<div class="settings-form-wrapper">
<div class="section-header-row">
<div class="section-title-box">
<mat-icon class="section-icon">tune</mat-icon>
@ -145,6 +146,22 @@
}
</div>
</div>
} @else {
<div class="plugin-disabled-card">
<div class="plugin-disabled-icon-box">
<mat-icon class="plugin-disabled-icon">pause_circle_outline</mat-icon>
</div>
<div class="plugin-disabled-content">
<h3 class="plugin-disabled-title">Plugin actualmente inactivo</h3>
<p class="plugin-disabled-text">
Este plugin se encuentra desactivado. Sus comandos, reglas contextuales y badges están pausados y ocultos para los clientes del sistema.
</p>
<p class="plugin-disabled-hint">
Para ver o modificar sus parámetros de configuración y reactivar sus funciones, activa el interruptor superior.
</p>
</div>
</div>
}
} @else {
<div class="no-plugin-box">
<mat-icon class="no-plugin-icon">info</mat-icon>

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ChangeDetectorRef, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PluginManifest, PluginService, SchemaField } from '@services/plugin.service';
import { ConfigService } from '@services/config.service';
@ -20,6 +20,7 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
editingScripts: Record<string, boolean> = {};
private pluginSub?: Subscription;
private cdr = inject(ChangeDetectorRef);
private sanitizer = inject(DomSanitizer);
private http = inject(HttpClient);
private configService = inject(ConfigService);
@ -53,10 +54,15 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
this.schema = this.plugin.schema;
this.configData = this.pluginService.getPluginConfig(this.pluginName);
}
this.cdr.markForCheck();
}
toggleEnabled(checked: boolean): void {
if (!this.pluginName) return;
if (this.plugin) {
this.plugin.enabled = checked;
this.cdr.markForCheck();
}
this.pluginService.togglePlugin(this.pluginName, checked).subscribe({
next: () => {
const statusText = checked ? 'activado' : 'desactivado';

View File

@ -166,14 +166,12 @@
</span>
</mat-list-item>
@if (pluginService.hasPlugins()) {
<mat-list-item routerLink="/plugins" (click)="onItemClick()"
matTooltip="Plugins y Extensiones" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">extension</mat-icon>
<span>Plugins</span>
</span>
</mat-list-item>
}
<mat-list-item routerLink="/plugins" (click)="onItemClick()"
matTooltip="Plugins y Extensiones" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">extension</mat-icon>
<span>Plugins</span>
</span>
</mat-list-item>
}
</mat-nav-list>

View File

@ -162,7 +162,11 @@ export class PluginService {
const manifestRequests = plugins.map(p =>
this.http.get<Partial<PluginManifest>>(`${apiUrl}/plugins/${p.name}/manifest`).pipe(
catchError(() => of({})),
map(extra => ({ ...p, ...extra }))
map(extra => ({
...p,
...extra,
enabled: Boolean(p.enabled) // La base de datos es la única fuente de verdad para el estado de activación
}))
)
);
@ -191,7 +195,7 @@ export class PluginService {
const currentLang = lang || this.translateService.currentLang || this.translateService.defaultLang || 'es';
const apiUrl = this.configService.apiUrl;
for (const plugin of this.activePlugins) {
for (const plugin of this.allPlugins) {
// 1. Si el plugin declara i18n directamente en el manifest, lo inyectamos
if (plugin.i18n && typeof plugin.i18n === 'object') {
for (const [l, dict] of Object.entries(plugin.i18n)) {
@ -265,6 +269,10 @@ export class PluginService {
}
hasPlugins(): boolean {
return this.allPlugins.length > 0;
}
hasActivePlugins(): boolean {
return this.getPlugins().length > 0;
}