Creada nueva opción para gestionar CRUD de assets en la pantalla de menus
testing/ogGui-multibranch/pipeline/head There was a failure building this commit
Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit
Details
parent
383c385c2f
commit
46ebd5e618
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue