UNPKG

@pega/dx-component-builder-sdk

Version:

Utility for building custom UI components

459 lines (416 loc) 17.7 kB
import { Component, OnInit, Input, NgZone, forwardRef, OnDestroy, ViewChild, ElementRef } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatMenuModule } from '@angular/material/menu'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import download from 'downloadjs'; import { AngularPConnectData, AngularPConnectService } from '@pega/angular-sdk-components'; import { Utils } from '@pega/angular-sdk-components'; import { ComponentMapperComponent } from '@pega/angular-sdk-components'; import { PConnFieldProps } from '@pega/angular-sdk-components'; interface AttachmentProps extends Omit<PConnFieldProps, 'value'> { // If any, enter additional props that only exist on this component value: any; extensions: any; allowMultiple: boolean; } @Component({ selector: 'app-attachment', templateUrl: './attachment.component.html', styleUrls: ['./attachment.component.scss'], standalone: true, imports: [CommonModule, MatProgressSpinnerModule, MatMenuModule, MatIconModule, MatButtonModule, forwardRef(() => ComponentMapperComponent)] }) export class AttachmentComponent implements OnInit, OnDestroy { @Input() pConn$: typeof PConnect; @Input() formGroup$: FormGroup; // For interaction with AngularPConnect angularPConnectData: AngularPConnectData = {}; @ViewChild('uploader', { static: false }) fileInput: ElementRef; label$ = ''; value$: any; bRequired$ = false; bReadonly$ = false; bDisabled$ = false; bVisible$ = true; bLoading$ = false; bShowSelector$ = true; att_categoryName: string; fileTemp: any = {}; caseID: any; allowMultiple$ = false; extensions$ = ''; status = ''; validateMessage: string | undefined = ''; valueRef: string; imagePath$: string; localizedVal = PCore.getLocaleUtils().getLocaleValue; localeCategory = 'CosmosFields'; uploadMultipleFilesLabel = this.localizedVal('file_upload_text_multiple', this.localeCategory); uploadSingleFileLabel = this.localizedVal('file_upload_text_one', this.localeCategory); filesWithError: any = []; files: any = []; categoryName: string; displayMode: string | undefined; srcImg: any; deleteIcon: string; tempFilesToBeUploaded: any[]; constructor( private angularPConnect: AngularPConnectService, private utils: Utils, private ngZone: NgZone ) {} ngOnInit(): void { // // First thing in initialization is registering and subscribing to the AngularPConnect service this.angularPConnectData = this.angularPConnect.registerAndSubscribeComponent(this, this.onStateChange); this.caseID = PCore.getStoreValue('.pyID', 'caseInfo.content', this.pConn$.getContextName()); this.srcImg = this.utils.getImageSrc('document-doc', this.utils.getSDKStaticContentUrl()); this.deleteIcon = this.utils.getImageSrc('trash', this.utils.getSDKStaticContentUrl()); this.checkAndUpdate(); this.getAttachments(); } getAttachments() { let tempUploadedFiles = this.getCurrentAttachmentsList(this.getAttachmentKey(this.valueRef), this.pConn$.getContextName()); tempUploadedFiles = tempUploadedFiles.filter(f => f.label === this.valueRef && f.delete !== true); this.files?.map(f => { return f.responseProps?.pzInsKey && !f.responseProps.pzInsKey.includes('temp') ? { ...f, props: { ...f.props, onDelete: () => this.deleteFile(f) } } : { ...f }; }); this.files = [...this.files, ...tempUploadedFiles]; PCore.getPubSubUtils().subscribe( PCore.getConstants().PUB_SUB_EVENTS.CASE_EVENTS.ASSIGNMENT_SUBMISSION, this.resetAttachmentStoredState.bind(this), this.caseID ); return () => { PCore.getPubSubUtils().unsubscribe(PCore.getConstants().PUB_SUB_EVENTS.CASE_EVENTS.ASSIGNMENT_SUBMISSION, this.caseID); }; } checkAndUpdate() { // Should always check the bridge to see if the component should // update itself (re-render) const bUpdateSelf = this.angularPConnect.shouldComponentUpdate(this); // ONLY call updateSelf when the component should update if (bUpdateSelf) { this.updateSelf(); } } // Callback passed when subscribing to store change onStateChange() { this.checkAndUpdate(); } updateSelf() { const configProps: AttachmentProps = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()) as AttachmentProps; const stateProps = this.pConn$.getStateProps(); const { value, label, extensions, displayMode } = configProps; if (configProps.required != null) { this.bRequired$ = this.utils.getBooleanValue(configProps.required); } if (configProps.visibility != null) { this.bVisible$ = this.utils.getBooleanValue(configProps.visibility); } // disabled if (configProps.disabled != undefined) { this.bDisabled$ = this.utils.getBooleanValue(configProps.disabled); } if (configProps.readOnly != null) { this.bReadonly$ = this.utils.getBooleanValue(configProps.readOnly); } if (configProps.allowMultiple != null) { this.allowMultiple$ = this.utils.getBooleanValue(configProps.allowMultiple); } this.label$ = label; this.value$ = value; this.status = stateProps.status; this.validateMessage = this.angularPConnectData.validateMessage; this.extensions$ = extensions; this.valueRef = this.pConn$.getStateProps().value; this.valueRef = this.valueRef.startsWith('.') ? this.valueRef.substring(1) : this.valueRef; this.displayMode = displayMode; /* this is a temporary fix because required is supposed to be passed as a boolean and NOT as a string */ let { required, disabled } = configProps; [required, disabled] = [required, disabled].map(prop => prop === true || (typeof prop === 'string' && prop === 'true')); this.categoryName = ''; if (value && value.pyCategoryName) { this.categoryName = value.pyCategoryName; } if (value?.pxResults && +value.pyCount > 0) { this.files = value.pxResults.map(f => this.buildFilePropsFromResponse(f)); } this.updateAttachments(); } buildFilePropsFromResponse(respObj) { return { props: { meta: `${respObj.pyCategoryName}, ${respObj.pxCreateOperator}`, name: respObj.pyAttachName, icon: this.utils.getIconFromFileType(respObj.pyMimeFileExtension) }, responseProps: { ...respObj } }; } updateAttachments() { if (this.files.length > 0 && this.displayMode !== 'DISPLAY_ONLY') { const currentAttachmentList = this.getCurrentAttachmentsList(this.getAttachmentKey(this.valueRef), this.pConn$.getContextName()); // block duplicate files to redux store when added 1 after another to prevent multiple duplicates being added to the case on submit const tempFiles = this.files.filter(f => currentAttachmentList.findIndex(fr => fr.ID === f.ID) === -1 && !f.inProgress && f.responseProps); const updatedAttList = [...currentAttachmentList, ...tempFiles]; this.updateAttachmentState(this.pConn$, this.getAttachmentKey(this.valueRef), updatedAttList); } } resetAttachmentStoredState() { PCore.getStateUtils().updateState(this.pConn$?.getContextName(), this.getAttachmentKey(this.valueRef), undefined, { pageReference: 'context_data', isArrayDeepMerge: false }); } downloadFile(fileObj: any) { PCore.getAttachmentUtils() // @ts-ignore - 3rd parameter "responseEncoding" should be optional .downloadAttachment(fileObj.pzInsKey, this.pConn$.getContextName()) .then((content: any) => { const extension = fileObj.pyAttachName.split('.').pop(); this.fileDownload(content.data, fileObj.pyFileName, extension); }) .catch(e => { console.log(e); }); } fileDownload = (data, fileName, ext) => { const file = ext ? `${fileName}.${ext}` : fileName; download(atob(data), file); }; getAttachmentKey = (name = '') => (name ? `attachmentsList.${name}` : 'attachmentsList'); getCurrentAttachmentsList(key, context) { return PCore.getStoreValue(`.${key}`, 'context_data', context) || []; } validateMaxSize(fileObj, maxSizeInMB): boolean { const fileSize = (fileObj.size / 1048576).toFixed(2); return parseFloat(fileSize) < parseFloat(maxSizeInMB); } validateFileExtension = (fileObj, allowedExtensions) => { if (!allowedExtensions) { return true; } const allowedExtensionList = allowedExtensions .toLowerCase() .split(',') .map(item => item.replaceAll('.', '').trim()); const extension = fileObj.name.split('.').pop().toLowerCase(); return allowedExtensionList.includes(extension); }; updateAttachmentState(pConn, key, attachments) { PCore.getStateUtils().updateState(this.pConn$.getContextName(), key, attachments, { pageReference: 'context_data', isArrayDeepMerge: false }); } deleteFile(file) { const attachmentsList: any[] = []; let currentAttachmentList = this.getCurrentAttachmentsList(this.getAttachmentKey(this.valueRef), this.pConn$.getContextName()); // If file to be deleted is the one added in previous stage i.e. for which a file instance is created in server // no need to filter currentAttachmentList as we will get another entry of file in redux with delete & label // eslint-disable-next-line no-unsafe-optional-chaining if (this.value$ && this.value$?.pxResults && +this.value$?.pyCount > 0 && file.responseProps && file?.responseProps?.pzInsKey !== 'temp') { const updatedAttachments = this.files.map(f => { if (f.responseProps && f.responseProps.pzInsKey === file.responseProps.pzInsKey) { return { ...f, delete: true, label: this.valueRef }; } return f; }); // updating the redux store to help form-handler in passing the data to delete the file from server this.updateAttachmentState(this.pConn$, this.getAttachmentKey(this.valueRef), [...updatedAttachments]); const newlyAddedFiles = this.files.filter(f => !!f.ID); const filesPostDelete = this.files.filter( f => f.responseProps?.pzInsKey !== 'temp' && f.responseProps?.pzInsKey !== file.responseProps?.pzInsKey ); this.files = [...filesPostDelete, ...newlyAddedFiles]; } // if the file being deleted is the added in this stage i.e. whose data is not yet created in server else { // filter newly added files in this stage, later the updated current stage files will be added to redux once files state is updated currentAttachmentList = currentAttachmentList.filter(f => f.ID !== file.ID); this.files = this.files.filter(f => f.ID !== file.ID); this.updateAttachmentState(this.pConn$, this.getAttachmentKey(this.valueRef), [...currentAttachmentList, ...attachmentsList]); if (file.inProgress) { // @ts-ignore - 3rd parameter "responseEncoding" should be optional PCore.getAttachmentUtils().cancelRequest(file.ID, this.pConn$.getContextName()); } } this.filesWithError = this.filesWithError?.filter(f => f.ID !== file.ID); if (this.filesWithError.length === 0) { this.clearFieldErrorMessages(); } // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.fileInput && this.fileInput.nativeElement.value ? null : ''; } onFileAdded(event) { let addedFiles = Array.from(event.target.files); addedFiles = this.allowMultiple$ ? addedFiles : [addedFiles[0]]; const maxAttachmentSize = PCore.getEnvironmentInfo().getMaxAttachmentSize() || '5'; this.tempFilesToBeUploaded = [ ...addedFiles.map((f: any, index) => { f.ID = `${new Date().getTime()}I${index}`; f.inProgress = true; f.props = { type: f.type, name: f.name, icon: this.utils.getIconFromFileType(f.type), onDelete: () => this.deleteFile(f) }; if (!this.validateMaxSize(f, maxAttachmentSize)) { f.props.error = true; f.inProgress = false; f.props.meta = this.pConn$.getLocalizedValue(`File is too big. Max allowed size is ${maxAttachmentSize}MB.`, '', ''); } else if (!this.validateFileExtension(f, this.extensions$)) { f.props.error = true; f.inProgress = false; f.props.meta = `${this.pConn$.getLocalizedValue( 'File has invalid extension. Allowed extensions are:', '', '' )} ${this.extensions$.replaceAll('.', '')}`; } if (f.props.error) { const fieldName = this.pConn$.getStateProps().value; const context = this.pConn$.getContextName(); PCore.getMessageManager().addMessages({ messages: [ { type: 'error', message: this.pConn$.getLocalizedValue('Error with one or more files', '', '') } ], property: fieldName, pageReference: this.pConn$.getPageReference(), context }); } return f; }) ]; const tempFilesWithError = this.tempFilesToBeUploaded.filter(f => f.props.error); if (tempFilesWithError.length > 0) { this.filesWithError = tempFilesWithError; } if (!this.allowMultiple$) { this.files = [...this.tempFilesToBeUploaded]; } else { this.files = [...this.files, ...this.tempFilesToBeUploaded]; } this.uploadFiles(); } clearFieldErrorMessages() { const fieldName = this.pConn$.getStateProps().value; const context = this.pConn$.getContextName(); PCore.getMessageManager().clearMessages({ type: PCore.getConstants().MESSAGES.MESSAGES_TYPE_ERROR, property: fieldName, pageReference: this.pConn$.getPageReference(), context }); } onUploadProgress() {} errorHandler(isFetchCanceled, attachedFile) { return error => { if (!isFetchCanceled(error)) { let uploadFailMsg = this.pConn$.getLocalizedValue('Something went wrong', '', ''); if (error.response && error.response.data && error.response.data.errorDetails) { uploadFailMsg = this.pConn$.getLocalizedValue(error.response.data.errorDetails[0].localizedValue, '', ''); } this.files.map(f => { if (f.ID === attachedFile.ID) { f.props.meta = uploadFailMsg; f.props.error = true; f.props.onDelete = () => this.deleteFile(f); f.props.icon = this.utils.getIconFromFileType(f.type); f.props.name = this.pConn$.getLocalizedValue('Unable to upload file', '', ''); f.inProgress = false; const fieldName = this.pConn$.getStateProps().value; const context = this.pConn$.getContextName(); // set errors to property to block submit even on errors in file upload PCore.getMessageManager().addMessages({ messages: [ { type: 'error', message: this.pConn$.getLocalizedValue('Error with one or more files', '', '') } ], property: fieldName, pageReference: this.pConn$.getPageReference(), context }); delete f.props.progress; } return f; }); } throw error; }; } uploadFiles() { const filesToBeUploaded = this.files .filter(e => { const isFileUploaded = e.props && e.props.progress === 100; const fileHasError = e.props && e.props.error; const isFileUploadedinLastStep = e.responseProps && e.responseProps.pzInsKey; return !isFileUploaded && !fileHasError && !isFileUploadedinLastStep; }) .map(f => window.PCore.getAttachmentUtils().uploadAttachment( f, () => { this.onUploadProgress(); }, isFetchCanceled => { return this.errorHandler(isFetchCanceled, f); }, this.pConn$.getContextName() ) ); Promise.allSettled(filesToBeUploaded) .then((fileResponses: any) => { fileResponses = fileResponses.filter(fr => fr.status !== 'rejected'); // in case of deleting an in progress file, promise gets cancelled but still enters then block if (fileResponses.length > 0) { this.files.forEach(f => { const index = fileResponses.findIndex((fr: any) => fr.value.clientFileID === f.ID); if (index >= 0) { f.props.meta = this.pConn$.getLocalizedValue('Uploaded successfully', '', ''); f.props.progress = 100; f.inProgress = false; f.handle = fileResponses[index].value.ID; f.label = this.valueRef; f.category = this.categoryName; f.responseProps = { pzInsKey: 'temp', pyAttachName: f.props.name }; } }); this.updateAttachments(); if (this.filesWithError?.length === 0) { this.clearFieldErrorMessages(); } } }) .catch(error => { console.log(error); }); } ngOnDestroy(): void { if (this.angularPConnectData.unsubscribeFn) { this.angularPConnectData.unsubscribeFn(); } PCore.getPubSubUtils().unsubscribe(PCore.getConstants().PUB_SUB_EVENTS.CASE_EVENTS.ASSIGNMENT_SUBMISSION, this.caseID); } }