diff --git a/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/po-dynamic-form-field.interface.ts b/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/po-dynamic-form-field.interface.ts index 6f61b69a5d..16629a569e 100644 --- a/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/po-dynamic-form-field.interface.ts +++ b/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/po-dynamic-form-field.interface.ts @@ -12,6 +12,7 @@ import { PoMultiselectFilterMode, PoMultiselectLiterals, PoSwitchLabelPosition, + PoUploadFile, PoUploadFileRestrictions, PoUploadLiterals } from '../../po-field'; @@ -20,6 +21,7 @@ import { PoLookupColumn } from '../../po-field/po-lookup/interfaces/po-lookup-co import { PoMultiselectOption } from '../../po-field/po-multiselect/po-multiselect-option.interface'; import { PoSelectOption } from '../../po-field/po-select/po-select-option.interface'; import { ForceBooleanComponentEnum, ForceOptionComponentEnum } from '../po-dynamic-field-force-component.enum'; +import { PoProgressAction } from '../../po-progress/'; import { Observable } from 'rxjs'; import { PoDynamicField } from '../po-dynamic-field.interface'; @@ -691,6 +693,46 @@ export interface PoDynamicFormField extends PoDynamicField { */ showRequired?: boolean; + /** + * Define uma ação personalizada no componente `po-upload`, adicionando um botão no canto inferior direito + * de cada barra de progresso associada aos arquivos enviados ou em envio. + * + * **Componente compatível**: `po-upload`, + * + * **Exemplo de configuração**: + * ```typescript + * customAction: { + * label: 'Baixar', + * icon: 'ph-download', + * type: 'default', + * visible: true, + * disabled: false + * }; + * ``` + */ + customAction?: PoProgressAction; + + /** + * Evento emitido ao clicar na ação personalizada configurada no `p-custom-action`. + * + * **Componente compatível**: `po-upload`, + * + * Este evento é emitido quando o botão de ação personalizada é clicado na barra de progresso associada a um arquivo. + * O arquivo relacionado à barra de progresso será passado como parâmetro do evento, permitindo executar operações específicas para aquele arquivo. + * + * **Parâmetro do evento**: + * - `file`: O arquivo associado ao botão de ação. Este objeto é da classe `PoUploadFile` e contém informações sobre o arquivo, como nome, status e progresso. + * + * **Exemplo de uso**: + * ```typescript + * customActionClick: (file: PoUploadFile) => { + * console.log('Ação personalizada clicada para o arquivo:', file.name); + * // Lógica de download ou outra ação relacionada ao arquivo + * } + * ``` + */ + customActionClick?: (file: PoUploadFile) => void; + /** * Evento será disparado quando ocorrer algum erro no envio do arquivo. * > Por parâmetro será passado o objeto do retorno que é do tipo `HttpErrorResponse`. diff --git a/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/po-dynamic-form-fields/po-dynamic-form-fields.component.html b/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/po-dynamic-form-fields/po-dynamic-form-fields.component.html index c3514045a0..b4c75f5858 100644 --- a/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/po-dynamic-form-fields/po-dynamic-form-fields.component.html +++ b/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/po-dynamic-form-fields/po-dynamic-form-fields.component.html @@ -432,6 +432,8 @@ [p-label]="field.label" [p-literals]="field.literals" [name]="field.property" + [p-custom-action]="field.customAction" + (p-custom-action-click)="field.customActionClick($event)" (p-error)="field.onError($event)" (p-success)="field.onSuccess($event)" (p-upload)="field.onUpload($event)" diff --git a/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/samples/sample-po-dynamic-form-container/sample-po-dynamic-form-container.component.ts b/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/samples/sample-po-dynamic-form-container/sample-po-dynamic-form-container.component.ts index d48427a140..62b6c2bf10 100644 --- a/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/samples/sample-po-dynamic-form-container/sample-po-dynamic-form-container.component.ts +++ b/projects/ui/src/lib/components/po-dynamic/po-dynamic-form/samples/sample-po-dynamic-form-container/sample-po-dynamic-form-container.component.ts @@ -5,7 +5,8 @@ import { PoDynamicFormFieldChanged, PoDynamicFormValidation, PoNotificationService, - ForceBooleanComponentEnum + ForceBooleanComponentEnum, + PoUploadFile } from '@po-ui/ng-components'; import { PoDynamicFormContainerService } from './sample-po-dynamic-form-container.service'; @@ -193,7 +194,11 @@ export class SamplePoDynamicFormContainerComponent implements OnInit { gridSmColumns: 12, label: 'Upload your background', optional: true, - url: 'https://po-sample-api.onrender.com/v1/uploads/addFile' + url: 'https://po-sample-api.onrender.com/v1/uploads/addFile', + customAction: { icon: 'ph ph-download', visible: true }, + customActionClick: (file: PoUploadFile) => { + console.log('Iniciar download para o arquivo:', file.name); + } } ]; diff --git a/projects/ui/src/lib/components/po-field/po-upload/po-upload-base.component.spec.ts b/projects/ui/src/lib/components/po-field/po-upload/po-upload-base.component.spec.ts index 1ad4ce5500..8c32e3c829 100644 --- a/projects/ui/src/lib/components/po-field/po-upload/po-upload-base.component.spec.ts +++ b/projects/ui/src/lib/components/po-field/po-upload/po-upload-base.component.spec.ts @@ -13,6 +13,7 @@ import * as ValidatorsFunctions from '../validators'; import { PoUploadBaseComponent, poUploadLiteralsDefault } from './po-upload-base.component'; import { PoUploadFile } from './po-upload-file'; import { PoUploadService } from './po-upload.service'; +import { PoProgressAction } from '../../po-progress'; @Component({ selector: 'po-upload', @@ -501,6 +502,15 @@ describe('PoUploadBaseComponent:', () => { expect(files.splice).not.toHaveBeenCalled(); }); + + it('callCustomAction: should emit customActionClick event with the provided file', () => { + const mockFile = { name: 'mock-file.txt', size: 12345 } as PoUploadFile; + spyOn(component.customActionClick, 'emit'); + + component.customActionClick.emit(mockFile); + + expect(component.customActionClick.emit).toHaveBeenCalledWith(mockFile); + }); }); describe('Properties:', () => { @@ -770,5 +780,45 @@ describe('PoUploadBaseComponent:', () => { expect(component.isMultiple).toBe(true); }); + + it('p-custom-action: should assign a valid PoProgressAction object', () => { + const validAction: PoProgressAction = { + label: 'Download', + icon: 'ph-download', + type: 'default', + disabled: false, + visible: true + }; + + component.customAction = validAction; + + expect(component.customAction).toEqual(validAction); + }); + + it('p-custom-action: should handle undefined or null values for customAction', () => { + const invalidValues = [null, undefined]; + invalidValues.forEach(value => { + component.customAction = value; + fixture.detectChanges(); + + expect(component.customAction).toBeFalsy(); + }); + }); + + it('p-custom-action: should handle partial PoProgressAction objects', () => { + const partialAction: PoProgressAction = { label: 'Partial Action' }; + component.customAction = partialAction; + + expect(component.customAction).toEqual(partialAction); + }); + + it('p-custom-action-click: should emit event when called', () => { + const mockFile = { name: 'mock-file.txt', size: 12345 } as PoUploadFile; + spyOn(component.customActionClick, 'emit'); + + component.customActionClick.emit(mockFile); + + expect(component.customActionClick.emit).toHaveBeenCalledWith(mockFile); + }); }); }); diff --git a/projects/ui/src/lib/components/po-field/po-upload/po-upload-base.component.ts b/projects/ui/src/lib/components/po-field/po-upload/po-upload-base.component.ts index aadb52d536..73e5343f93 100644 --- a/projects/ui/src/lib/components/po-field/po-upload/po-upload-base.component.ts +++ b/projects/ui/src/lib/components/po-field/po-upload/po-upload-base.component.ts @@ -11,6 +11,7 @@ import { PoUploadLiterals } from './interfaces/po-upload-literals.interface'; import { PoUploadFile } from './po-upload-file'; import { PoUploadStatus } from './po-upload-status.enum'; import { PoUploadService } from './po-upload.service'; +import { PoProgressAction } from '../../po-progress'; export const poUploadLiteralsDefault = { en: { @@ -239,6 +240,86 @@ export abstract class PoUploadBaseComponent implements ControlValueAccessor, Val @Input({ alias: 'p-required-url', transform: convertToBoolean }) requiredUrl: boolean = true; + /** + * @optional + * + * @description + * + * Define uma ação personalizada no componente `po-upload`, adicionando um botão no canto inferior direito + * de cada barra de progresso associada aos arquivos enviados ou em envio. + * + * A ação deve implementar a interface **PoProgressAction**, permitindo configurar propriedades como: + * - `label`: Texto do botão. + * - `icon`: Ícone a ser exibido no botão. + * - `type`: Tipo de botão (ex.: `danger` ou `default`). + * - `disabled`: Indica se o botão deve estar desabilitado. + * - `visible`: Indica se o botão deve estar visível. + * + * **Exemplo de uso:** + * + * ```html + * + * + * ``` + * + * ```typescript + * customAction: PoProgressAction = { + * label: 'Baixar', + * icon: 'ph ph-download', + * type: 'default', + * visible: true + * }; + * + * onCustomActionClick(file: PoUploadFile) { + * console.log(`Ação personalizada clicada para o arquivo: ${file.name}`); + * } + * ``` + */ + @Input('p-custom-action') customAction?: PoProgressAction; + + /** + * @optional + * + * @description + * + * Evento emitido ao clicar na ação personalizada configurada no `p-custom-action`. + * + * O evento retorna o arquivo associado à barra de progresso onde a ação foi clicada, + * permitindo executar operações específicas para aquele arquivo. + * + * **Exemplo de uso:** + * + * ```html + * + * + * ``` + * + * ```typescript + * customAction: PoProgressAction = { + * label: 'Baixar', + * icon: 'ph ph-download', + * type: 'default', + * visible: true + * }; + * + * onCustomActionClick(file: PoUploadFile) { + * console.log(`Ação personalizada clicada para o arquivo: ${file.name}`); + * // Lógica para download do arquivo + * this.downloadFile(file); + * } + * + * downloadFile(file: PoUploadFile) { + * // Exemplo de download + * console.log(`Iniciando o download do arquivo: ${file.name}`); + * } + * ``` + */ + @Output('p-custom-action-click') customActionClick: EventEmitter = new EventEmitter(); + /** * @optional * diff --git a/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.html b/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.html index a6edc89413..9962b138b5 100644 --- a/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.html +++ b/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.html @@ -69,6 +69,8 @@ [p-status]="progressStatusByFileStatus[file.status]" [p-text]="file.displayName" [p-value]="file.percent" + [p-custom-action]="customAction" + (p-custom-action-click)="customClick(file)" (p-cancel)="cancel(file)" (p-retry)="uploadFiles([file])" > diff --git a/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.spec.ts b/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.spec.ts index e7c2b308be..df6dc40af5 100644 --- a/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.spec.ts +++ b/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.spec.ts @@ -20,6 +20,7 @@ import { PoUploadFile } from './po-upload-file'; import { PoUploadFileRestrictionsComponent } from './po-upload-file-restrictions/po-upload-file-restrictions.component'; import { PoUploadService } from './po-upload.service'; import { PoUploadStatus } from './po-upload-status.enum'; +import { PoProgressAction } from '../../po-progress'; describe('PoUploadComponent:', () => { let component: PoUploadComponent; @@ -1015,6 +1016,28 @@ describe('PoUploadComponent:', () => { ); expect(component.renderer.removeAttribute).toHaveBeenCalledTimes(1); }); + + it('customClick: should emit customActionClick with the provided file if customAction is defined', () => { + const mockFile = { name: 'mock-file.txt' } as PoUploadFile; + component.customAction = { label: 'Download', icon: 'ph-download' } as PoProgressAction; + + spyOn(component.customActionClick, 'emit'); + + component.customClick(mockFile); + + expect(component.customActionClick.emit).toHaveBeenCalledWith(mockFile); + }); + + it('customClick: should not emit customActionClick if customAction is undefined', () => { + const mockFile = { name: 'mock-file.txt' } as PoUploadFile; + component.customAction = undefined; + + spyOn(component.customActionClick, 'emit'); + + component.customClick(mockFile); + + expect(component.customActionClick.emit).not.toHaveBeenCalled(); + }); }); describe('Templates:', () => { diff --git a/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.ts b/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.ts index 45c1bae39e..1c8be3bf5c 100644 --- a/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.ts +++ b/projects/ui/src/lib/components/po-field/po-upload/po-upload.component.ts @@ -47,6 +47,11 @@ import { PoUploadService } from './po-upload.service'; * * * + * + * + * + * + * */ @Component({ selector: 'po-upload', @@ -341,6 +346,12 @@ export class PoUploadComponent extends PoUploadBaseComponent implements AfterVie ); } + customClick(file: PoUploadFile) { + if (this.customAction) { + this.customActionClick.emit(file); + } + } + private cleanInputValue() { this.calledByCleanInputValue = true; this.inputFile.nativeElement.value = ''; diff --git a/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-download/sample-po-upload-download.component.html b/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-download/sample-po-upload-download.component.html new file mode 100644 index 0000000000..6fd87f4596 --- /dev/null +++ b/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-download/sample-po-upload-download.component.html @@ -0,0 +1,8 @@ + diff --git a/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-download/sample-po-upload-download.component.ts b/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-download/sample-po-upload-download.component.ts new file mode 100644 index 0000000000..bfa77bbd2e --- /dev/null +++ b/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-download/sample-po-upload-download.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { PoProgressAction } from '@po-ui/ng-components'; + +@Component({ + selector: 'sample-po-upload-download', + templateUrl: 'sample-po-upload-download.component.html' +}) +export class SamplePoUploadDownloadComponent { + customAction: PoProgressAction = { + icon: 'ph ph-download', + type: 'default', + visible: false + }; + + uploadSuccess() { + this.customAction.visible = true; + } + + onCustomActionClick(file: { rawFile: File }) { + if (!file.rawFile) { + console.error('Arquivo inválido ou não encontrado.'); + return; + } + + this.downloadFile(file.rawFile); + } + + downloadFile(rawFile: File) { + // Cria uma URL temporária para o arquivo + const url = URL.createObjectURL(rawFile); + + // Cria um link temporário para iniciar o download + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = rawFile.name; // Define o nome do arquivo para o download + anchor.style.display = 'none'; + + // Adiciona o link ao DOM, aciona o clique e remove o link + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + + // Libera a memória utilizada pela URL temporária + URL.revokeObjectURL(url); + } +} diff --git a/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-labs/sample-po-upload-labs.component.html b/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-labs/sample-po-upload-labs.component.html index 18f8306686..c17cbe8c20 100644 --- a/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-labs/sample-po-upload-labs.component.html +++ b/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-labs/sample-po-upload-labs.component.html @@ -22,6 +22,8 @@ [p-restrictions]="restrictions" [p-url]="url" [p-headers]="headers" + [p-custom-action]="action" + (p-custom-action-click)="changeEvent('p-custom-action-click')" (p-error)="changeEvent('p-error')" (p-success)="changeEvent('p-success')" (p-upload)="changeEvent('p-upload')" @@ -147,7 +149,19 @@ -
+
+ +
+ + + + + + +
+
+ +
diff --git a/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-labs/sample-po-upload-labs.component.ts b/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-labs/sample-po-upload-labs.component.ts index 79c9197cd9..603b1cb54c 100644 --- a/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-labs/sample-po-upload-labs.component.ts +++ b/projects/ui/src/lib/components/po-field/po-upload/samples/sample-po-upload-labs/sample-po-upload-labs.component.ts @@ -1,6 +1,13 @@ import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; -import { PoCheckboxGroupOption, PoUploadFileRestrictions, PoUploadLiterals } from '@po-ui/ng-components'; +import { + PoCheckboxGroupOption, + PoProgressAction, + PoSelectOption, + PoUploadFileRestrictions, + PoUploadLiterals +} from '@po-ui/ng-components'; @Component({ selector: 'sample-po-upload-labs', @@ -24,6 +31,9 @@ export class SamplePoUploadLabsComponent implements OnInit { url: string; headers: { [name: string]: string | Array }; headersLabs: string; + action: PoProgressAction; + actionForm: FormGroup; + public readonly propertiesOptions: Array = [ { value: 'autoupload', label: 'Automatic upload' }, { value: 'directory', label: 'Directory' }, @@ -37,11 +47,45 @@ export class SamplePoUploadLabsComponent implements OnInit { { value: 'showRequired', label: 'Show Required' }, { value: 'restrictionsInfo', label: 'Hide Restrictions Info' }, { value: 'selectButton', label: 'Hide Select Files Button' }, - { value: 'sendButton', label: 'Hide Send Files Button' } + { value: 'sendButton', label: 'Hide Send Files Button' }, + { value: 'showCustomAction', label: 'Add Custom Action to Progress' } + ]; + + public readonly typeOptions: Array = [ + { label: 'Danger', value: 'danger' }, + { label: 'Default', value: 'default' } ]; + public readonly iconOptions: Array = [ + { value: 'ph ph-download', label: 'ph ph-download' }, + { value: 'ph ph-Server', label: 'ph ph-Server' }, + { value: 'ph ph-upload', label: 'ph ph-upload' }, + { value: 'ph ph-share', label: 'ph ph-share' } + ]; + + constructor(private fb: FormBuilder) { + this.initializeActionForm(); + } + + initializeActionForm() { + this.actionForm = this.fb.group({ + label: [''], + icon: [''], + type: ['default'], + visible: [true], + disabled: [false] + }); + } + ngOnInit() { this.restore(); + this.actionForm.valueChanges.subscribe(formValue => { + this.updateAction(formValue); + }); + } + + updateAction(formValue: any) { + this.action = formValue; } changeEvent(event: string) { @@ -98,6 +142,8 @@ export class SamplePoUploadLabsComponent implements OnInit { this.url = 'https://po-sample-api.onrender.com/v1/uploads/addFile'; this.headers = undefined; this.headersLabs = undefined; + this.actionForm.reset({ type: 'default', visible: true }); + this.action = { label: '', type: 'default' }; } private getValueInBytes(value: number) { diff --git a/projects/ui/src/lib/components/po-progress/index.ts b/projects/ui/src/lib/components/po-progress/index.ts index 2b18f79afd..d0c74d96a8 100644 --- a/projects/ui/src/lib/components/po-progress/index.ts +++ b/projects/ui/src/lib/components/po-progress/index.ts @@ -1,5 +1,7 @@ export * from './enums/po-progress-status.enum'; export * from './enums/po-progress-size.enum'; +export * from './interfaces/'; + export * from './po-progress.component'; export * from './po-progress.module'; diff --git a/projects/ui/src/lib/components/po-progress/interfaces/index.ts b/projects/ui/src/lib/components/po-progress/interfaces/index.ts new file mode 100644 index 0000000000..53f27ac16e --- /dev/null +++ b/projects/ui/src/lib/components/po-progress/interfaces/index.ts @@ -0,0 +1 @@ +export * from './po-progress-actions.interface'; diff --git a/projects/ui/src/lib/components/po-progress/interfaces/po-progress-actions.interface.ts b/projects/ui/src/lib/components/po-progress/interfaces/po-progress-actions.interface.ts new file mode 100644 index 0000000000..ab2a49713d --- /dev/null +++ b/projects/ui/src/lib/components/po-progress/interfaces/po-progress-actions.interface.ts @@ -0,0 +1,88 @@ +import { TemplateRef } from '@angular/core'; + +/** + * @description + * Interface para as ações dos componentes po-progress e po-upload. + * + * @usedBy PoProgressComponent, PoUploadComponent + */ +export interface PoProgressAction { + /** Rótulo da ação. */ + label?: string; + + /** + * @description + * + * Define um ícone que será exibido ao lado esquerdo do rótulo. + * + * É possível usar qualquer um dos ícones da [Biblioteca de ícones](https://po-ui.io/icons). conforme exemplo abaixo: + * ``` + * + * + * ``` + * + * Também é possível utilizar outras fontes de ícones, por exemplo a biblioteca Font Awesome, da seguinte forma: + * ``` + * + * + * ``` + * + * Outra opção seria a customização do ícone através do `TemplateRef`, conforme exemplo abaixo: + * component.html: + * ``` + * + * + * + * + * + * ``` + * component.ts: + * ``` + * @ViewChild('iconTemplate', { static: true } ) iconTemplate : TemplateRef; + * + * myProperty = [ + * { + * label: 'FA ICON', + * icon: this.iconTemplate + * } + * ]; + * ``` + */ + icon?: string | TemplateRef; + + /** + * Função que deve retornar um booleano para habilitar ou desabilitar a ação para o registro selecionado. + * + * Também é possível informar diretamente um valor booleano que vai habilitar ou desabilitar a ação para todos os registros. + */ + disabled?: boolean | Function; + + /** + * @description + * + * Define a cor do item, sendo `default` o padrão. + * + * Valores válidos: + * - `default` + * - `danger` - indicado para ações exclusivas (excluir, sair). + */ + type?: string; + + /** + * @description + * + * Define se a ação será visível. + * + * > Caso o valor não seja especificado a ação será visível. + * + * Opções para tornar a ação visível ou não: + * + * - Função que deve retornar um booleano. + * + * - Informar diretamente um valor booleano. + * + */ + visible?: boolean | Function; +} diff --git a/projects/ui/src/lib/components/po-progress/po-progress-base.component.spec.ts b/projects/ui/src/lib/components/po-progress/po-progress-base.component.spec.ts index 94495fd71f..b1887634c2 100644 --- a/projects/ui/src/lib/components/po-progress/po-progress-base.component.spec.ts +++ b/projects/ui/src/lib/components/po-progress/po-progress-base.component.spec.ts @@ -59,6 +59,69 @@ describe('PoProgressBaseComponent:', () => { expectPropertiesValues(component, 'size', invalidValues, 'large'); }); + + describe('p-custom-action:', () => { + it('should accept a valid PoProgressAction', () => { + const validCustomAction = { + label: 'Download', + icon: 'ph ph-download', + type: 'default', + visible: true, + disabled: false + }; + + component.customAction = validCustomAction; + + expect(component.customAction).toEqual(validCustomAction); + }); + + it('should handle undefined or null values for customAction', () => { + const invalidValues = [null, undefined]; + + invalidValues.forEach(value => { + component.customAction = value; + expect(component.customAction).toBeFalsy(); + }); + }); + + it('should respect the visible property when it is a boolean', () => { + component.customAction = { label: 'Download', visible: true }; + + expect(component.customAction.visible).toBeTrue(); + }); + + it('should respect the visible property when it is a function', () => { + component.customAction = { label: 'Download', visible: () => false }; + + const isVisible = (component.customAction.visible as Function)(); + + expect(isVisible).toBeFalse(); + }); + + it('should respect the disabled property when it is a boolean', () => { + component.customAction = { label: 'Download', disabled: true }; + + expect(component.customAction.disabled).toBeTrue(); + }); + + it('should respect the disabled property when it is a function', () => { + component.customAction = { label: 'Download', disabled: () => true }; + + const isDisabled = (component.customAction.disabled as Function)(); + + expect(isDisabled).toBeTrue(); + }); + }); + describe('p-custom-action-click:', () => { + it('should emit when the event is triggered', () => { + spyOn(component.customActionClick, 'emit'); + + const mockFile = { name: 'example.txt' }; + component.customActionClick.emit(mockFile); + + expect(component.customActionClick.emit).toHaveBeenCalledWith(mockFile); + }); + }); }); describe('Methods:', () => { diff --git a/projects/ui/src/lib/components/po-progress/po-progress-base.component.ts b/projects/ui/src/lib/components/po-progress/po-progress-base.component.ts index 8f46b72849..b900d91567 100644 --- a/projects/ui/src/lib/components/po-progress/po-progress-base.component.ts +++ b/projects/ui/src/lib/components/po-progress/po-progress-base.component.ts @@ -4,6 +4,7 @@ import { convertToBoolean, convertToInt } from '../../utils/util'; import { PoProgressStatus } from './enums/po-progress-status.enum'; import { PoProgressSize } from './enums/po-progress-size.enum'; +import { PoProgressAction } from './interfaces'; const poProgressMaxValue = 100; const poProgressMinValue = 0; @@ -91,6 +92,87 @@ export class PoProgressBaseComponent { */ @Input('p-text') text?: string; + /** + * @optional + * + * @description + * + * Permite definir uma ação personalizada no componente `po-progress`, exibindo um botão no canto inferior direito + * da barra de progresso. A ação deve implementar a interface **PoProgressAction**, possibilitando configurar: + * + * - **`label`**: Texto exibido no botão (opcional). + * - **`icon`**: Ícone exibido no botão (opcional). + * - **`type`**: Tipo do botão (`default` ou `danger`) para indicar a intenção da ação (opcional). + * - **`disabled`**: Indica se o botão deve estar desabilitado (opcional). + * - **`visible`**: Determina se o botão será exibido. Pode ser um valor booleano ou uma função que retorna um booleano (opcional). + * + * @example + * **Exemplo de uso:** + * ```html + * + * ``` + * + * ```typescript + * customAction: PoProgressAction = { + * label: 'Baixar', + * icon: 'ph ph-download', + * type: 'default', + * visible: () => true + * }; + * + * onCustomActionClick() { + * console.log('Custom action triggered!'); + * } + * ``` + * + * **Cenários comuns:** + * 1. **Download de Arquivos**: Exibir um botão para realizar o download de um arquivo associado à barra de progresso. + * 2. **Cancelamento Personalizado**: Adicionar uma ação para interromper ou reverter uma operação em andamento. + */ + @Input('p-custom-action') customAction?: PoProgressAction; + + /** + * @optional + * + * @description + * + * Evento emitido quando o botão definido em `p-custom-action` é clicado. Este evento retorna informações + * relacionadas à barra de progresso ou ao arquivo/processo associado, permitindo executar ações específicas. + * + * @example + * **Exemplo de uso:** + * + * ```html + * + * ``` + * + * ```typescript + * customAction: PoProgressAction = { + * label: 'Cancelar', + * icon: 'ph ph-x', + * type: 'danger', + * visible: true + * }; + * + * onCustomActionClick() { + * console.log('Custom action triggered!'); + * } + * ``` + * + * **Cenários comuns:** + * 1. **Botão de Download**: Disparar o download do arquivo associado à barra de progresso. + * 2. **Ação Condicional**: Realizar uma validação ou chamada de API antes de prosseguir com a ação. + */ + @Output('p-custom-action-click') customActionClick: EventEmitter = new EventEmitter(); + /** * @optional * diff --git a/projects/ui/src/lib/components/po-progress/po-progress.component.html b/projects/ui/src/lib/components/po-progress/po-progress.component.html index e52e82f77b..3ad998f238 100644 --- a/projects/ui/src/lib/components/po-progress/po-progress.component.html +++ b/projects/ui/src/lib/components/po-progress/po-progress.component.html @@ -20,6 +20,7 @@
{{ value }}% + + + + { let component: PoProgressComponent; @@ -49,6 +50,66 @@ describe('PoProgressComponent:', () => { expect(component.retry.emit).toHaveBeenCalled(); }); + + it('callAction: should emit customActionClick event', () => { + spyOn(component.customActionClick, 'emit'); + + component.callAction(); + + expect(component.customActionClick.emit).toHaveBeenCalled(); + }); + + it('isActionVisible: should return true if visible is true or a function that returns true', () => { + component.customAction = { label: 'Action', visible: true }; + expect(component.isActionVisible(component.customAction)).toBeTrue(); + + component.customAction = { label: 'Action', visible: () => true }; + expect(component.isActionVisible(component.customAction)).toBeTrue(); + }); + + it('isActionVisible: should return false if visible is false or a function that returns false', () => { + component.customAction = { label: 'Action', visible: false }; + expect(component.isActionVisible(component.customAction)).toBeFalse(); + + component.customAction = { label: 'Action', visible: () => false }; + expect(component.isActionVisible(component.customAction)).toBeFalse(); + }); + + it('isActionVisible: should return true if action.icon is defined and action.visible is true', () => { + component.customAction = { icon: 'ph ph-icon', visible: true }; + expect(component.isActionVisible(component.customAction)).toBeTrue(); + }); + + it('isActionVisible: should return false if action.icon is defined but action.visible is false', () => { + component.customAction = { icon: 'ph ph-icon', visible: false }; + expect(component.isActionVisible(component.customAction)).toBeFalse(); + }); + + it('isActionVisible: should return true if action.icon is defined and action.visible is a function that returns true', () => { + component.customAction = { icon: 'ph ph-icon', visible: () => true }; + expect(component.isActionVisible(component.customAction)).toBeTrue(); + }); + + it('isActionVisible: should return false if action.icon is defined and action.visible is a function that returns false', () => { + component.customAction = { icon: 'ph ph-icon', visible: () => false }; + expect(component.isActionVisible(component.customAction)).toBeFalse(); + }); + + it('actionIsDisabled: should return true if disabled is true or a function that returns true', () => { + component.customAction = { label: 'Action', disabled: true }; + expect(component.actionIsDisabled(component.customAction)).toBeTrue(); + + component.customAction = { label: 'Action', disabled: () => true }; + expect(component.actionIsDisabled(component.customAction)).toBeTrue(); + }); + + it('actionIsDisabled: should return false if disabled is false or a function that returns false', () => { + component.customAction = { label: 'Action', disabled: false }; + expect(component.actionIsDisabled(component.customAction)).toBeFalse(); + + component.customAction = { label: 'Action', disabled: () => false }; + expect(component.actionIsDisabled(component.customAction)).toBeFalse(); + }); }); describe('Properties:', () => { @@ -127,6 +188,29 @@ describe('PoProgressComponent:', () => { expect(component.isAllowRetry).toBe(false); }); + + describe('p-custom-action:', () => { + it('should set customAction correctly when a valid object is assigned', () => { + const customAction = { + label: 'Download', + icon: 'ph ph-download', + type: 'default', + visible: true, + disabled: false + }; + + component.customAction = customAction; + + expect(component.customAction).toEqual(customAction); + }); + + it('should handle undefined or null values for customAction', () => { + [undefined, null].forEach(value => { + component.customAction = value; + expect(component.customAction).toBe(value); + }); + }); + }); }); describe('Templates:', () => { @@ -307,5 +391,85 @@ describe('PoProgressComponent:', () => { expect(progressInfo).toBe(null); }); + + it('should display customAction button with correct label and icon when customAction is defined and visible', () => { + component.customAction = { + label: 'Download', + icon: 'ph ph-download', + visible: true + }; + + fixture.detectChanges(); + + const customActionButton = nativeElement.querySelector('po-button'); + expect(customActionButton).toBeTruthy(); + expect(customActionButton.textContent.trim()).toBe('Download'); + expect(customActionButton.querySelector('.ph.ph-download')).toBeTruthy(); + }); + + it('should display customAction button when visible is undefined', () => { + component.customAction = { + icon: 'ph ph-download' + }; + + fixture.detectChanges(); + + const customActionButton = nativeElement.querySelector('po-button'); + expect(customActionButton).toBeTruthy(); + }); + + it('should not display customAction button when customAction is not visible', () => { + component.customAction = { + label: 'Hidden Action', + visible: false + }; + + fixture.detectChanges(); + + const customActionButton = nativeElement.querySelector('po-button'); + expect(customActionButton).toBeFalsy(); + }); + + it('should emit customActionClick event when customAction button is clicked', () => { + component.customAction = { label: 'Download', icon: 'download', type: 'default', visible: true }; + spyOn(component.customActionClick, 'emit'); + + fixture.detectChanges(); + + const customButton = nativeElement.querySelector('.po-progress-custom-button'); + expect(customButton).toBeTruthy(); + + component.callAction(); + expect(component.customActionClick.emit).toHaveBeenCalled(); + }); + + it('should disable customAction button when disabled is true', () => { + component.customAction = { + icon: 'download', + label: 'Download', + visible: true, + disabled: true + }; + + fixture.detectChanges(); + + const customActionButton = nativeElement.querySelector('.po-progress-custom-button button'); + expect(customActionButton).toBeTruthy(); + + expect(customActionButton.disabled).toBeTrue(); + }); + + it('should not disable customAction button when disabled is false', () => { + component.customAction = { + label: 'Enabled Action', + visible: true, + disabled: false + }; + + fixture.detectChanges(); + + const customActionButton = nativeElement.querySelector('.po-progress-custom-button button'); + expect(customActionButton.hasAttribute('disabled')).toBeFalse(); + }); }); }); diff --git a/projects/ui/src/lib/components/po-progress/po-progress.component.ts b/projects/ui/src/lib/components/po-progress/po-progress.component.ts index e7cc965e71..ac5108b49a 100644 --- a/projects/ui/src/lib/components/po-progress/po-progress.component.ts +++ b/projects/ui/src/lib/components/po-progress/po-progress.component.ts @@ -4,6 +4,9 @@ import { PoProgressBaseComponent } from './po-progress-base.component'; import { PoProgressStatus } from './enums/po-progress-status.enum'; import { PoLanguageService } from '../../services/po-language/po-language.service'; import { poProgressLiterals } from './literals/po-progress.literals'; +import { isExternalLink, isTypeof, openExternalLink } from '../../utils/util'; +import { PoProgressAction } from './interfaces'; +import { Router } from '@angular/router'; /** * @docsExtends PoProgressBaseComponent @@ -58,6 +61,7 @@ export class PoProgressComponent extends PoProgressBaseComponent implements OnIn } private poLanguageService = inject(PoLanguageService); + private router = inject(Router); ngOnInit(): void { this.language = this.poLanguageService.getShortLanguage(); @@ -74,4 +78,22 @@ export class PoProgressComponent extends PoProgressBaseComponent implements OnIn emitRetry() { this.retry.emit(); } + + actionIsDisabled(action: any) { + return isTypeof(action.disabled, 'function') ? action.disabled(action) : action.disabled; + } + + callAction(): void { + this.customActionClick.emit(); + } + + isActionVisible(action: any) { + if (action && (action.label || action.icon)) { + return action.visible !== undefined + ? isTypeof(action.visible, 'function') + ? action.visible() + : !!action.visible + : true; + } + } } diff --git a/projects/ui/src/lib/components/po-progress/samples/sample-po-progress-labs/sample-po-progress-labs.component.html b/projects/ui/src/lib/components/po-progress/samples/sample-po-progress-labs/sample-po-progress-labs.component.html index 76975c027b..8360122e22 100644 --- a/projects/ui/src/lib/components/po-progress/samples/sample-po-progress-labs/sample-po-progress-labs.component.html +++ b/projects/ui/src/lib/components/po-progress/samples/sample-po-progress-labs/sample-po-progress-labs.component.html @@ -1,59 +1,77 @@ - - +
+ - + + + - + +
+
+ + + + +
- +
+ + +
- -
- - - - - - - - - - - - - - - - - - + + + + + + +
+ +
+ + + + + + +
- +
diff --git a/projects/ui/src/lib/components/po-progress/samples/sample-po-progress-labs/sample-po-progress-labs.component.ts b/projects/ui/src/lib/components/po-progress/samples/sample-po-progress-labs/sample-po-progress-labs.component.ts index 162f4ee9ea..1382c10654 100644 --- a/projects/ui/src/lib/components/po-progress/samples/sample-po-progress-labs/sample-po-progress-labs.component.ts +++ b/projects/ui/src/lib/components/po-progress/samples/sample-po-progress-labs/sample-po-progress-labs.component.ts @@ -1,6 +1,15 @@ import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; -import { PoCheckboxGroupOption, PoProgressStatus, PoRadioGroupOption, PoProgressSize } from '@po-ui/ng-components'; +import { + PoCheckboxGroupOption, + PoProgressStatus, + PoRadioGroupOption, + PoProgressSize, + PoProgressAction, + PoSelectOption, + PoNotificationService +} from '@po-ui/ng-components'; @Component({ selector: 'sample-po-progress-labs', @@ -10,11 +19,16 @@ export class SamplePoProgressLabsComponent implements OnInit { event: any; info: string; infoIcon: string; - properties: Array; + disabledCancel: boolean; + indeterminate: boolean; + showPercentage: boolean; status: PoProgressStatus; size: PoProgressSize = PoProgressSize.large; text: string; value: number; + action: PoProgressAction; + actionForm: FormGroup; + showAction: false; infoIconsOptions: Array = [ { label: 'ph ph-warning-circle', value: 'ph ph-warning-circle' }, @@ -23,12 +37,6 @@ export class SamplePoProgressLabsComponent implements OnInit { { label: 'ph ph-cloud-slash', value: 'ph ph-cloud-slash' } ]; - propertiesOptions: Array = [ - { label: 'Disabled Cancel', value: 'disabledCancel' }, - { label: 'Indeterminate', value: 'indeterminate' }, - { label: 'Show percentage', value: 'showPercentage' } - ]; - statusOptions: Array = [ { label: 'Default', value: PoProgressStatus.Default }, { label: 'Success', value: PoProgressStatus.Success }, @@ -40,8 +48,49 @@ export class SamplePoProgressLabsComponent implements OnInit { { label: 'Large', value: PoProgressSize.large } ]; + public readonly typeOptions: Array = [ + { label: 'Danger', value: 'danger' }, + { label: 'Default', value: 'default' } + ]; + + public readonly iconOptions: Array = [ + { value: 'ph ph-download', label: 'ph ph-download' }, + { value: 'ph ph-Server', label: 'ph ph-Server' }, + { value: 'ph ph-upload', label: 'ph ph-upload' }, + { value: 'ph ph-share', label: 'ph ph-share' } + ]; + + public readonly actionOptions: Array = [ + { label: 'Disabled', value: 'disabled' }, + { label: 'Visible', value: 'visible' } + ]; + + constructor( + private fb: FormBuilder, + private poNotification: PoNotificationService + ) { + this.initializeActionForm(); + } + + initializeActionForm() { + this.actionForm = this.fb.group({ + label: [''], + icon: [''], + type: ['default'], + visible: [true], + disabled: [false] + }); + } + ngOnInit() { this.restore(); + this.actionForm.valueChanges.subscribe(formValue => { + this.updateAction(formValue); + }); + } + + updateAction(formValue: any) { + this.action = formValue; } onEvent(event) { @@ -52,10 +101,15 @@ export class SamplePoProgressLabsComponent implements OnInit { this.event = undefined; this.info = undefined; this.infoIcon = undefined; - this.properties = []; + this.disabledCancel = false; + this.indeterminate = false; + this.showPercentage = false; this.status = undefined; this.text = undefined; this.value = undefined; this.size = PoProgressSize.large; + this.actionForm.reset({ type: 'default', visible: true }); + this.action = { label: '', type: 'default' }; + this.showAction = false; } }