Merge pull request 'upload-assets' (#54) from upload-assets into main
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/head This commit looks good Details

Reviewed-on: #54
pull/55/head 1.2.3
Juan Manuel Bardallo 2026-03-17 13:20:51 +01:00
commit efee90bd3e
9 changed files with 367 additions and 4 deletions

View File

@ -1,4 +1,8 @@
# Changelog
## [1.2.3] - 2026-03-17
### Added
- Se ha creado una nueva opción para gestionar assets en la pantalla de menus.
## [1.2.2] - 2026-03-17
### Added
- Se ha integrado un editor interactivo en texto plano para los menús HTML directamente en la pantalla de edición, permitiendo previsualizar y modificar el código en vivo sin necesidad de resubir archivos. Utiliza la sintaxis moderna `@if/@else` de Angular 17+ para su lógica de renderizado.

View File

@ -114,6 +114,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
import { MatSortModule } from '@angular/material/sort';
import { MenusComponent } from './components/menus/menus.component';
import { AssetManagerModalComponent } from './components/menus/asset-manager-modal/asset-manager-modal.component';
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component';
import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component';
import { ExportImageComponent } from './components/images/export-image/export-image.component';
@ -171,7 +172,6 @@ import { HardwareProfileComponent } from './components/hardware-profile/hardware
import { HardwareCollectionModalComponent } from './components/hardware-profile/hardware-collection-modal/hardware-collection-modal.component';
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
@ -298,7 +298,8 @@ registerLocaleData(localeEs, 'es-ES');
HardwareProfileComponent,
HardwareCollectionModalComponent,
HardwareTypeComponent,
ShowDetailsComponent
ShowDetailsComponent,
AssetManagerModalComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -0,0 +1,51 @@
.asset-actions-bar {
display: flex;
align-items: center;
margin-bottom: 1rem;
gap: 8px;
}
.spacer {
flex: 1 1 auto;
}
.assets-list-container {
height: 400px;
overflow: auto;
position: relative;
border: 1px solid #ccc;
border-radius: 4px;
}
.loading-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(255,255,255,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
font-weight: bold;
}
table {
width: 100%;
}
.folder-link {
font-weight: bold;
color: #1976d2;
text-decoration: none;
cursor: pointer;
}
.folder-link:hover {
text-decoration: underline;
}
.empty-state {
text-align: center;
padding: 2rem;
color: #666;
font-style: italic;
}

View File

@ -0,0 +1,94 @@
<h2 mat-dialog-title>
Gestionar Assets: public/assets/<strong>{{ currentPath }}</strong>
</h2>
<mat-dialog-content>
<div class="asset-actions-bar">
<!-- NAVEGACIÓN -->
<button mat-icon-button (click)="goUp()" [disabled]="!currentPath || currentPath === ''" title="Subir nivel">
<mat-icon>arrow_upward</mat-icon>
</button>
<button mat-icon-button (click)="loadAssets()" title="Refrescar">
<mat-icon>refresh</mat-icon>
</button>
<!-- SUBIDA DE ARCHIVOS -->
<span class="spacer"></span>
<input type="file" #fileInput (change)="onFileSelected($event)" style="display: none;">
<button mat-stroked-button color="primary" (click)="fileInput.click()" [disabled]="isUploading">
<mat-icon>upload_file</mat-icon>
Subir Fichero
</button>
</div>
<!-- MOSTRAR PROGRESO SI ESTÁ SUBIENDO -->
@if (isUploading) {
<mat-progress-bar mode="indeterminate" style="margin-bottom: 10px;"></mat-progress-bar>
}
<!-- TABLA DE ARCHIVOS -->
<div class="assets-list-container">
@if (isLoading) {
<div class="loading-overlay">Cargando...</div>
}
<table mat-table [dataSource]="assets" class="mat-elevation-z1">
<!-- Icon/Type Column -->
<ng-container matColumnDef="icon">
<th mat-header-cell *matHeaderCellDef> Tipo </th>
<td mat-cell *matCellDef="let element">
<mat-icon [color]="element.type === 'folder' ? 'accent' : 'primary'">
{{ element.type === 'folder' ? 'folder' : 'insert_drive_file' }}
</mat-icon>
</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Nombre </th>
<td mat-cell *matCellDef="let element">
@if (element.type === 'folder') {
<a href="javascript:void(0)" (click)="enterFolder(element.name)" class="folder-link">
{{ element.name }}
</a>
} @else {
<span>{{ element.name }}</span>
}
</td>
</ng-container>
<!-- Size Column -->
<ng-container matColumnDef="size">
<th mat-header-cell *matHeaderCellDef> Tamaño </th>
<td mat-cell *matCellDef="let element">
{{ element.type === 'file' ? formatBytes(element.size) : '--' }}
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: right;"> Acciones </th>
<td mat-cell *matCellDef="let element" style="text-align: right;">
@if (element.type === 'file') {
<button mat-icon-button (click)="copyPath(element.path)" title="Copiar ruta al portapapeles">
<mat-icon>content_copy</mat-icon>
</button>
}
<button mat-icon-button color="warn" (click)="deleteAsset(element.path)" title="Eliminar">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell empty-state" colspan="4">No hay archivos en este directorio</td>
</tr>
</table>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">Cerrar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AssetManagerModalComponent } from './asset-manager-modal.component';
describe('AssetManagerModalComponent', () => {
let component: AssetManagerModalComponent;
let fixture: ComponentFixture<AssetManagerModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AssetManagerModalComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AssetManagerModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,143 @@
import { Component, OnInit } from '@angular/core';
import { MatDialogRef, MatDialog } from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr';
import { DataService } from '../data.service';
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
export interface AssetNode {
name: string;
type: 'folder' | 'file';
size: number | null;
path: string;
}
@Component({
selector: 'app-asset-manager-modal',
templateUrl: './asset-manager-modal.component.html',
styleUrls: ['./asset-manager-modal.component.css']
})
export class AssetManagerModalComponent implements OnInit {
currentPath: string = '';
assets: AssetNode[] = [];
isLoading: boolean = false;
isUploading: boolean = false;
displayedColumns: string[] = ['icon', 'name', 'size', 'actions'];
constructor(
public dialogRef: MatDialogRef<AssetManagerModalComponent>,
private dialog: MatDialog,
private dataService: DataService,
private toastService: ToastrService
) { }
ngOnInit(): void {
this.loadAssets();
}
loadAssets(): void {
this.isLoading = true;
this.dataService.getAssets(this.currentPath).subscribe({
next: (data) => {
this.assets = data;
this.isLoading = false;
},
error: (err) => {
this.isLoading = false;
this.toastService.error('Error al cargar la ruta actual.');
console.error(err);
// If we failed traversing down, fallback to root
if (this.currentPath !== '') {
this.currentPath = '';
this.loadAssets();
}
}
});
}
enterFolder(folderName: string): void {
if (this.currentPath === '') {
this.currentPath = folderName;
} else {
this.currentPath = this.currentPath + '/' + folderName;
}
this.loadAssets();
}
goUp(): void {
if (this.currentPath === '') return;
const parts = this.currentPath.split('/');
parts.pop();
this.currentPath = parts.join('/');
this.loadAssets();
}
onFileSelected(event: any): void {
const file = event.target.files[0];
if (file) {
const isZip = file.name.toLowerCase().endsWith('.zip');
this.isUploading = true;
this.dataService.uploadAssets(file, this.currentPath, isZip).subscribe({
next: (response) => {
this.isUploading = false;
this.toastService.success(response.message || 'Fichero subido correctamente.');
this.loadAssets(); // Refresh directory
},
error: (error) => {
this.isUploading = false;
this.toastService.error(error?.error?.message || 'Error al subir el fichero.');
console.error(error);
}
});
}
// Reset input so it triggers (change) next time even if it's the same file name
event.target.value = '';
}
deleteAsset(path: string): void {
this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: path }
}).afterClosed().subscribe((result) => {
if (result) {
this.isLoading = true;
this.dataService.deleteAsset(path).subscribe({
next: (res) => {
this.toastService.success('Elemento eliminado correctamente.');
this.loadAssets();
},
error: (err) => {
this.isLoading = false;
this.toastService.error(err?.error?.message || 'No se pudo eliminar el elemento.');
}
});
}
});
}
copyPath(path: string): void {
// Generate an absolute-like path starting with /assets/
const fullUrl = `/assets/${path}`;
navigator.clipboard.writeText(fullUrl).then(() => {
this.toastService.info(`Enlace copiado: ${fullUrl}`);
}).catch(err => {
console.error('Error al copiar al portapapeles', err);
});
}
formatBytes(bytes: number | null): string {
if (bytes === null || bytes === 0) return '0 Bytes';
const k = 1024;
const dm = 2;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
close(): void {
this.dialogRef.close();
}
}

View File

@ -32,7 +32,7 @@ export class DataService {
}),
catchError(error => {
console.error('Error fetching commands', error);
return throwError(error);
return throwError(() => error);
})
);
}
@ -48,10 +48,44 @@ export class DataService {
}),
catchError(error => {
console.error('Error fetching menus', error);
return throwError(error);
return throwError(() => error);
})
);
}
getAssets(path: string = ''): Observable<any[]> {
const params = new HttpParams().set('path', path);
return this.http.get<any[]>(`${this.baseUrl}/ogassets/list`, { params }).pipe(
catchError(error => {
console.error('Error fetching assets', error);
return throwError(() => error);
})
);
}
uploadAssets(file: File, path: string = '', extractZip: boolean = false): Observable<any> {
const formData = new FormData();
formData.append('file', file);
formData.append('path', path);
formData.append('extractZip', String(extractZip));
return this.http.post<any>(`${this.baseUrl}/ogassets/upload`, formData).pipe(
catchError(error => {
console.error('Error uploading assets', error);
return throwError(() => error);
})
);
}
deleteAsset(path: string): Observable<any> {
return this.http.delete<any>(`${this.baseUrl}/ogassets/delete`, {
body: { path }
}).pipe(
catchError(error => {
console.error('Error deleting asset', error);
return throwError(() => error);
})
);
}
}

View File

@ -7,6 +7,7 @@
Administrar menús</h2>
</div>
<div class="images-button-row">
<button class="action-button" (click)="openAssetManager()" style="margin-right: 10px;">Gestionar assets</button>
<button class="action-button" (click)="addMenu()" joyrideStep="addStep"
text="Utiliza este botón para añadir un nuevo menu.">Añadir menú</button>
</div>

View File

@ -8,6 +8,7 @@ import { JoyrideService } from "ngx-joyride";
import { Router } from "@angular/router";
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
import { CreateMenuComponent } from "./create-menu/create-menu.component";
import { AssetManagerModalComponent } from "./asset-manager-modal/asset-manager-modal.component";
import { ConfigService } from '@services/config.service';
@Component({
@ -85,6 +86,17 @@ export class MenusComponent implements OnInit {
});
}
openAssetManager(): void {
const dialogRef = this.dialog.open(AssetManagerModalComponent, {
width: '800px',
height: '600px'
});
dialogRef.afterClosed().subscribe(() => {
// Nothing specific to do upon close for now
});
}
search(): void {
this.loading = true;
this.http.get<any>(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(