import { Component, OnInit, Input, NgZone, forwardRef, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import download from 'downloadjs'; import debounce from 'lodash.debounce'; import { AngularPConnectData, AngularPConnectService } from '@pega/angular-sdk-components'; import { Utils } from '@pega/angular-sdk-components'; import { ComponentMapperComponent } from '@pega/angular-sdk-components'; import { getResolvedConstantValue } from '@pega/angular-sdk-components'; interface FileUtilityProps { // If any, enter additional props that only exist on this component label?: string; caseId?: string; } @Component({ selector: 'app-file-utility', templateUrl: './file-utility.component.html', styleUrls: ['./file-utility.component.scss'], imports: [CommonModule, MatButtonModule, MatFormFieldModule, MatInputModule, forwardRef(() => ComponentMapperComponent)] }) export class FileUtilityComponent implements OnInit, OnDestroy { @Input() pConn$: typeof PConnect; // For interaction with AngularPConnect angularPConnectData: AngularPConnectData = {}; arFullListAttachments: any[] = []; lu_name$ = ''; lu_icon$ = ''; lu_bLoading$ = false; lu_count$ = 0; lu_arActions$: any[] = []; lu_arItems$: any = []; va_arItems$: any[] = []; lu_onViewAllFunction: any; bShowFileModal$ = false; bShowLinkModal$ = false; bShowViewAllModal$ = false; arFileMainButtons$: any[] = []; arFileSecondaryButtons$: any[] = []; arLinkMainButtons$: any[] = []; arLinkSecondaryButtons$: any[] = []; arFiles$: any[] = []; arFileList$: any[] = []; removeFileFromList$: any; arLinks$: any[] = []; arLinksList$: any[] = []; removeLinksFromList$: any; link_title$ = ''; link_url$ = ''; closeSvgIcon$ = ''; currentCaseID = ''; debouncedGetAttachments: any; attachSubId; addAttachmentsActions: 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); const configProps: FileUtilityProps = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()); const { caseId, label } = configProps; this.lu_name$ = label ?? ''; this.lu_icon$ = 'paper-clip'; this.closeSvgIcon$ = this.utils.getImageSrc('times', this.utils.getSDKStaticContentUrl()); this.addAttachmentsActions = [ { text: this.pConn$.getLocalizedValue('Add files', '', ''), id: 'addNewFiles', onClick: () => this.createModal('addLocalFile') }, { text: this.pConn$.getLocalizedValue('Add links', '', ''), id: 'addNewLinks', onClick: () => this.createModal('addLocalLink') } ]; // const onViewAllCallback = () => this.onViewAll(this.arFullListAttachments); this.lu_onViewAllFunction = { onClick: this.onViewAll.bind(this) }; this.removeFileFromList$ = { onClick: this.removeFileFromList.bind(this) }; this.removeLinksFromList$ = { onClick: this.removeLinksFromList.bind(this) }; this.debouncedGetAttachments = debounce(this.refreshAttachments.bind(this), 1000); this.updateSelf(); this.createModalButtons(); const caseID = caseId ?? getResolvedConstantValue(this.pConn$, PCore.getConstants().CASE_INFO.CASE_INFO_ID); const attachSubObject = { matcher: 'ATTACHMENTS', criteria: { ID: caseID } }; this.attachSubId = PCore.getMessagingServiceManager().subscribe(attachSubObject, this.debouncedGetAttachments, this.pConn$.getContextName()); this.debouncedGetAttachments(); } ngOnDestroy(): void { if (this.angularPConnectData.unsubscribeFn) { this.angularPConnectData.unsubscribeFn(); } PCore.getMessagingServiceManager().unsubscribe(this.attachSubId); } // Callback passed when subscribing to store change onStateChange() { // adding a property to track in configProps, when ever the attachment file changes // need to trigger a redraw this.pConn$.registerAdditionalProps({ lastRefreshTime: `@P ${PCore.getConstants().SUMMARY_OF_ATTACHMENTS_LAST_REFRESH_TIME}` }); // 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.caseHasChanged()) { this.updateSelf(); } } onAttachFiles(files: any[] = []) { const attachmentUtils = PCore.getAttachmentUtils(); const caseID = this.pConn$.getValue(PCore.getConstants().CASE_INFO.CASE_INFO_ID); if (files.length > 0) { this.lu_bLoading$ = true; } const arFiles: any = Array.from(files); Promise.allSettled( arFiles.map(file => attachmentUtils.uploadAttachment(file, this.onUploadProgress, this.errorHandler, this.pConn$.getContextName())) ) .then((fileResponses: any) => { const uploadedFiles: any = []; fileResponses.forEach(fileResponse => { if (fileResponse.status === 'fulfilled') { uploadedFiles.push(fileResponse.value); } }); if (uploadedFiles.length > 0) { (attachmentUtils.linkAttachmentsToCase(caseID, uploadedFiles, 'File', this.pConn$.getContextName()) as Promise) .then(() => { this.refreshAttachments(); }) .catch(); } }) .catch(); this.arFileList$ = []; this.lu_bLoading$ = false; } refreshAttachments() { this.updateSelf(); } onUploadProgress() {} errorHandler() {} onAttachLinks(links) { const attachmentUtils = PCore.getAttachmentUtils(); const caseID = this.pConn$.getValue(PCore.getConstants().CASE_INFO.CASE_INFO_ID); if (links.length > 0) { this.lu_bLoading$ = true; } const linksToAttach = links.map(link => ({ type: 'URL', category: 'URL', url: link.url, name: link.linkTitle })); (attachmentUtils.linkAttachmentsToCase(caseID, linksToAttach, 'URL', this.pConn$.getContextName()) as Promise) .then(() => { this.refreshAttachments(); }) .catch(console.log); } addAttachments(attsFromResp: any[] = []) { this.lu_bLoading$ = false; attsFromResp = attsFromResp.map(respAtt => { const updatedAtt = { ...respAtt, meta: `${respAtt.category} . ${this.utils.generateDateTime(respAtt.createTime, 'DateTime-Since')}, ${respAtt.createdBy}` }; if (updatedAtt.type === 'FILE') { updatedAtt.nameWithExt = updatedAtt.fileName; } return updatedAtt; }); return attsFromResp; } onViewAll(): void { this.bShowViewAllModal$ = true; // add clickAway listener // window.addEventListener('mouseup', this._clickAway.bind(this)); } // Below method is not needed, still having it commented in case we want in future // _clickAway(event: any) { // let bInPopUp = false; // // run through list of elements in path, if menu not in th path, then want to // // hide (toggle) the menu // for (const i in event.path) { // if (event.path[i].className == 'psdk-modal-file-top' || event.path[i].tagName == 'BUTTON') { // bInPopUp = true; // break; // } // } // if (!bInPopUp) { // this.bShowViewAllModal$ = false; // window.removeEventListener('mouseup', this._clickAway.bind(this)); // } // } _closeViewAll() { this.bShowViewAllModal$ = false; // window.removeEventListener('mouseup', this._clickAway.bind(this)); } removeFileFromList(item: any) { if (item != null) { const arFileList = this.arFileList$; const itemId = item.id; for (let fileIndex = 0; fileIndex < arFileList.length; fileIndex++) { if (arFileList[fileIndex].id == itemId) { // remove the file from the list and redraw this.ngZone.run(() => { arFileList.splice(fileIndex, 1); }); break; } } } } removeLinksFromList(item: any) { const localLinksList = this.arLinksList$.slice(); if (item != null) { const itemId = item.id; for (let linkIndex = 0; linkIndex < localLinksList.length; linkIndex++) { if (localLinksList[linkIndex].id == itemId) { // remove the file from the list and redraw localLinksList.splice(linkIndex, 1); this.ngZone.run(() => { this.arLinksList$ = localLinksList.slice(); }); break; } } } } getNewListUtilityItemProps = ({ att, cancelFile, downloadFile, deleteFile, removeFile }) => { let actions; let isDownloadable = false; if (att.progress && att.progress !== 100) { actions = [ { id: `Cancel-${att.ID}`, text: 'Cancel', icon: 'times', onClick: cancelFile } ]; } else if (att.links) { const isFile = att.type === 'FILE'; const ID = att.ID.replace(/\s/gi, ''); const actionsMap = new Map([ [ 'download', { id: `download-${ID}`, text: isFile ? 'Download' : 'Open', icon: isFile ? 'download' : 'open', onClick: downloadFile } ], [ 'delete', { id: `Delete-${ID}`, text: 'Delete', icon: 'trash', onClick: deleteFile } ] ]); actions = []; actionsMap.forEach((action, actionKey) => { if (att.links[actionKey]) { actions.push(action); } }); // eslint-disable-next-line @typescript-eslint/no-unused-vars isDownloadable = att.links.download; } else if (att.error) { actions = [ { id: `Remove-${att.ID}`, text: 'Remove', icon: 'trash', onClick: removeFile } ]; } return { id: att.ID, visual: { icon: this.utils.getIconForAttachment(att), progress: att.progress == 100 ? undefined : att.progress }, primary: { type: att.type, name: att.name, icon: 'trash', click: removeFile }, secondary: { text: att.meta }, actions }; }; getListUtilityItemProps = ({ att, cancelFile, downloadFile, deleteFile, removeFile }) => { let actions; let isDownloadable = false; if (att.progress && att.progress !== 100) { actions = [ { id: `Cancel-${att.ID}`, text: this.pConn$.getLocalizedValue('Cancel', '', ''), icon: 'times', onClick: cancelFile } ]; } else if (att.links) { const isFile = att.type === 'FILE'; const ID = att.ID.replace(/\s/gi, ''); const actionsMap = new Map([ [ 'download', { id: `download-${ID}`, text: isFile ? this.pConn$.getLocalizedValue('Download', '', '') : this.pConn$.getLocalizedValue('Open', '', ''), icon: isFile ? 'download' : 'open', onClick: downloadFile } ], [ 'delete', { id: `Delete-${ID}`, text: this.pConn$.getLocalizedValue('Delete', '', ''), icon: 'trash', onClick: deleteFile } ] ]); actions = []; actionsMap.forEach((action, actionKey) => { if (att.links[actionKey]) { actions.push(action); } }); // eslint-disable-next-line @typescript-eslint/no-unused-vars isDownloadable = att.links.download; } else if (att.error) { actions = [ { id: `Remove-${att.ID}`, text: this.pConn$.getLocalizedValue('Remove', '', ''), icon: 'trash', onClick: removeFile } ]; } return { id: att.ID, visual: { icon: this.utils.getIconForAttachment(att), progress: att.progress == 100 ? undefined : att.progress }, primary: { type: att.type, name: att.name, icon: 'open', click: downloadFile }, secondary: { text: att.meta }, actions }; }; _addLink() { // copy list locally const localList = this.arLinksList$.slice(); const url = this.link_url$; if (!/^(http|https):\/\//.test(this.link_url$)) { this.link_url$ = `http://${this.link_url$}`; } // list for display let oLink: any = {}; oLink.icon = 'chain'; oLink.ID = `${new Date().getTime()}`; oLink = this.getNewListUtilityItemProps({ att: oLink, downloadFile: null, cancelFile: null, deleteFile: null, removeFile: null }); oLink.type = 'URL'; oLink.primary.type = oLink.type; oLink.visual.icon = 'chain'; oLink.primary.name = this.link_title$; oLink.primary.icon = 'open'; oLink.secondary.text = url; this.ngZone.run(() => { // need to create a new array or summary list won't detect changes this.arLinksList$ = localList.slice(); this.arLinksList$.push(oLink); // list for actually attachments const link: any = {}; link.id = oLink.id; link.linkTitle = this.link_title$; link.type = oLink.type; link.url = url; this.arLinks$.push(link); // clear values this.link_title$ = ''; this.link_url$ = ''; }); } _changeTitle(event: any) { this.link_title$ = event.srcElement.value; } _changeUrl(event: any) { this.link_url$ = event.srcElement.value; } downloadFile(att: any) { const attachUtils = PCore.getAttachmentUtils(); const { ID, name, extension, type } = att; const context = this.pConn$.getContextName(); attachUtils // @ts-ignore - 3rd parameter "responseEncoding" is optional .downloadAttachment(ID, context) .then((content: any) => { if (type === 'FILE') { this.fileDownload(content.data, name, extension); } else if (type === 'URL') { let { data } = content; if (!/^(http|https):\/\//.test(data)) { data = `//${data}`; } window.open(content.data, '_blank'); } }) .catch(console.error); } fileDownload = (data, fileName, ext) => { const file = ext ? `${fileName}.${ext}` : fileName; download(atob(data), file); }; cancelFile() { alert('cancel'); } deleteFile(att: any) { setTimeout(() => { const attachUtils = PCore.getAttachmentUtils(); const { ID } = att; const context = this.pConn$.getContextName(); attachUtils .deleteAttachment(ID, context) .then(() => { this.updateSelf(); // let newAttachments; // setAttachments((current) => { // newAttachments = current.filter((file) => file.ID !== ID); // return newAttachments; // }); // if (callbackFn) { // callbackFn(newAttachments); // } }) .catch(console.error); }); } removeFile() { alert('remove'); } removeNewFile() { alert('remove'); } createModal(modalType: string) { switch (modalType) { case 'addLocalFile': this.ngZone.run(() => { this.bShowFileModal$ = true; }); break; case 'addLocalLink': this.ngZone.run(() => { this.bShowLinkModal$ = true; }); break; default: break; } } createModalButtons() { this.arFileMainButtons$.push({ actionID: 'attach', jsAction: 'attachFiles', name: this.pConn$.getLocalizedValue('Attach files', '', '') }); this.arFileSecondaryButtons$.push({ actionID: 'cancel', jsAction: 'cancel', name: this.pConn$.getLocalizedValue('Cancel', '', '') }); this.arLinkMainButtons$.push({ actionID: 'attach', jsAction: 'attachLinks', name: this.pConn$.getLocalizedValue('Attach links', '', '') }); this.arLinkSecondaryButtons$.push({ actionID: 'cancel', jsAction: 'cancel', name: this.pConn$.getLocalizedValue('Cancel', '', '') }); } uploadMyFiles($event) { // alert($event.target.files[0]); // outputs the first file this.arFiles$ = this.getFiles($event.target.files); // convert FileList to an array const myFiles = Array.from(this.arFiles$); this.arFileList$ = myFiles.map(att => { return this.getNewListUtilityItemProps({ att, downloadFile: !att.progress ? () => this.downloadFile(att) : null, cancelFile: att.progress ? () => this.cancelFile() : null, deleteFile: !att.progress ? () => this.deleteFile(att) : null, removeFile: att.error ? () => this.removeNewFile() : null }); }); } getFiles(arFiles: any[]): any[] { return this.setNewFiles(arFiles); } setNewFiles(arFiles) { let index = 0; for (const file of arFiles) { if (!this.validateMaxSize(file, 5)) { file.error = true; file.meta = 'File is too big. Max allowed size is 5MB.'; } file.mimeType = file.type; file.icon = this.utils.getIconFromFileType(file.type); file.ID = `${new Date().getTime()}I${index}`; index++; } return arFiles; } validateMaxSize(fileObj, maxSizeInMB): boolean { const fileSize = (fileObj.size / 1048576).toFixed(2); return fileSize < maxSizeInMB; } onFileActionButtonClick(event: any) { // modal buttons switch (event.action) { case 'cancel': this.bShowFileModal$ = false; this.clearOutFiles(); break; case 'attachFiles': this.bShowFileModal$ = false; this.onAttachFiles(this.arFiles$); this.clearOutFiles(); break; default: break; } } onLinkActionButtonClick(event: any) { // modal buttons switch (event.action) { case 'cancel': this.bShowLinkModal$ = false; this.clearOutLinks(); break; case 'attachLinks': this.bShowLinkModal$ = false; this.onAttachLinks(this.arLinks$); this.clearOutLinks(); break; default: break; } } clearOutFiles() { this.arFileList$ = []; this.arFiles$ = []; } clearOutLinks() { this.arLinksList$ = []; this.arLinks$ = []; this.link_title$ = ''; this.link_url$ = ''; } addALink() {} _fieldOnChangeLink(event: any) { this.link_title$ = event.target.value; } _fieldOnChangeURL(event: any) { this.link_url$ = event.target.value; } updateSelf() { const caseID = this.getCaseID(); if (caseID) { this.lu_bLoading$ = true; this.fetchCaseAttachments(caseID) .then(resp => { this.handleAttachmentsResponse(resp); }) .catch(err => { console.log(err); }) .finally(() => { this.lu_bLoading$ = false; }); } } getCaseID() { return this.pConn$.getValue(PCore.getConstants().CASE_INFO.CASE_INFO_ID) || ''; } fetchCaseAttachments(caseID) { const attachmentUtils = PCore.getAttachmentUtils(); return attachmentUtils.getCaseAttachments(caseID, this.pConn$.getContextName()); } handleAttachmentsResponse(resp) { this.arFullListAttachments = this.addAttachments(resp); this.lu_count$ = this.arFullListAttachments.length; this.lu_arActions$ = this.addAttachmentsActions; this.lu_arItems$ = this.getUtilityItems(this.arFullListAttachments.slice(0, 3)); this.va_arItems$ = this.getUtilityItems(this.arFullListAttachments); } getUtilityItems(attachments) { return attachments.map(this.mapAttachmentToUtilityItem.bind(this)); } mapAttachmentToUtilityItem(att) { return this.getListUtilityItemProps({ att, downloadFile: this.getDownloadFunction(att), cancelFile: this.getCancelFunction(att), deleteFile: this.getDeleteFunction(att), removeFile: this.getRemoveFunction(att) }); } getDownloadFunction(att) { return !att.progress ? () => this.downloadFile(att) : null; } getCancelFunction(att) { return att.progress ? () => this.cancelFile() : null; } getDeleteFunction(att) { return !att.progress ? () => this.deleteFile(att) : null; } getRemoveFunction(att) { return att.error ? () => this.removeFile() : null; } caseHasChanged(): boolean { const caseID = this.pConn$.getValue(PCore.getConstants().CASE_INFO.CASE_INFO_ID); if (this.currentCaseID !== caseID) { this.currentCaseID = caseID; return true; } return false; } }