From fc9c692e0b3ca39fc439513c1a5d9232ab1b867c Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 26 Sep 2025 11:37:15 +0300 Subject: [PATCH 1/2] UI: Add support UTF-8 symbols in export file name --- .../import-export/import-export.service.ts | 95 +++++++------------ 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 94960cb838..d7c6d5880a 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -22,7 +22,6 @@ import { AppState } from '@core/core.state'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { BreakpointId, Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; import { deepClone, guid, isDefined, isNotEmptyStr, isObject, isString, isUndefined } from '@core/utils'; -import { WINDOW } from '@core/services/window.service'; import { DOCUMENT } from '@angular/common'; import { AliasesInfo, @@ -100,8 +99,7 @@ type SupportEntityResources = 'includeResourcesInExportWidgetTypes' | 'includeRe @Injectable() export class ImportExportService { - constructor(@Inject(WINDOW) private window: Window, - @Inject(DOCUMENT) private document: Document, + constructor(@Inject(DOCUMENT) private document: Document, private store: Store, private translate: TranslateService, private dashboardService: DashboardService, @@ -177,9 +175,7 @@ export class ImportExportService { public exportCalculatedField(calculatedFieldId: string): void { this.calculatedFieldsService.getCalculatedFieldById(calculatedFieldId).subscribe({ next: (calculatedField) => { - let name = calculatedField.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), name); + this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), calculatedField.name, true); }, error: (e) => { this.handleExportError(e, 'calculated-fields.export-failed-error'); @@ -200,9 +196,7 @@ export class ImportExportService { this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportDashboard'); this.dashboardService.exportDashboard(dashboardId, result.include).subscribe({ next: (dashboard) => { - let name = dashboard.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareDashboardExport(dashboard), name); + this.exportToPc(this.prepareDashboardExport(dashboard), dashboard.title, true); }, error: (e) => { this.handleExportError(e, 'dashboard.export-failed-error'); @@ -261,9 +255,8 @@ export class ImportExportService { widgetTitle: string, breakpoint: BreakpointId) { const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint); const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName; - let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); - fileName = fileName.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareExport(widgetItem), fileName); + const fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); + this.exportToPc(this.prepareExport(widgetItem), fileName, true); } public importWidget(dashboard: Dashboard, targetState: string, @@ -360,9 +353,7 @@ export class ImportExportService { this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportWidgetTypes'); this.widgetService.exportWidgetType(widgetTypeId, result.include).subscribe({ next: (widgetTypeDetails) => { - let name = widgetTypeDetails.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareExport(widgetTypeDetails), name); + this.exportToPc(this.prepareExport(widgetTypeDetails), widgetTypeDetails.name, true); }, error: (e) => { this.handleExportError(e, 'widget-type.export-failed-error'); @@ -440,7 +431,7 @@ export class ImportExportService { public exportEntity(entityData: VersionedEntity): void { const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId; let fileName = (entityData as EntityInfoData).name; - let preparedData; + let preparedData: any; switch (id.entityType) { case EntityType.DEVICE_PROFILE: case EntityType.ASSET_PROFILE: @@ -511,9 +502,7 @@ export class ImportExportService { for (const widgetTypeDetails of widgetTypesDetails) { widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails)); } - let name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(widgetsBundleItem, name); + this.exportToPc(widgetsBundleItem, widgetsBundle.title, true); }, error: (e) => { this.handleExportError(e, 'widgets-bundle.export-failed-error'); @@ -528,9 +517,7 @@ export class ImportExportService { widgetsBundle: this.prepareExport(widgetsBundle), widgetTypeFqns }; - let name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(widgetsBundleItem, name); + this.exportToPc(widgetsBundleItem, widgetsBundle.title, true); }, error: (e) => { this.handleExportError(e, 'widgets-bundle.export-failed-error'); @@ -662,11 +649,9 @@ export class ImportExportService { private onRuleChainExported() { return { next: (ruleChainExport: RuleChainImport) => { - let name = ruleChainExport.ruleChain.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(ruleChainExport, name); + this.exportToPc(ruleChainExport, ruleChainExport.ruleChain.name, true); }, - error: (e) => { + error: (e: any) => { this.handleExportError(e, 'rulechain.export-failed-error'); } }; @@ -747,9 +732,7 @@ export class ImportExportService { public exportDeviceProfile(deviceProfileId: string) { this.deviceProfileService.exportDeviceProfile(deviceProfileId).subscribe({ next: (deviceProfile) => { - let name = deviceProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(deviceProfile), name); + this.exportToPc(this.prepareProfileExport(deviceProfile), deviceProfile.name, true); }, error: (e) => { this.handleExportError(e, 'device-profile.export-failed-error'); @@ -776,9 +759,7 @@ export class ImportExportService { public exportAssetProfile(assetProfileId: string) { this.assetProfileService.exportAssetProfile(assetProfileId).subscribe({ next: (assetProfile) => { - let name = assetProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(assetProfile), name); + this.exportToPc(this.prepareProfileExport(assetProfile), assetProfile.name, true); }, error: (e) => { this.handleExportError(e, 'asset-profile.export-failed-error'); @@ -805,9 +786,7 @@ export class ImportExportService { public exportTenantProfile(tenantProfileId: string) { this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe({ next: (tenantProfile) => { - let name = tenantProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(tenantProfile), name); + this.exportToPc(this.prepareProfileExport(tenantProfile), tenantProfile.name, true); }, error: (e) => { this.handleExportError(e, 'tenant-profile.export-failed-error'); @@ -882,7 +861,7 @@ export class ImportExportService { jsZip.generateAsync({type: 'blob'}).then(content => { this.downloadFile(content, filename, ZIP_TYPE); exportJsSubjectSubject.next(null); - }).catch(e => { + }).catch((e: any) => { exportJsSubjectSubject.error(e); }); } catch (e) { @@ -1180,42 +1159,40 @@ export class ImportExportService { )); } - private exportToPc(data: any, filename: string) { + private exportToPc(data: any, filename: string, normalizeFileName = false) { if (!data) { console.error('No data'); return; } - this.exportJson(data, filename); + this.exportJson(data, filename, normalizeFileName); } - public exportJson(data: any, filename: string) { + public exportJson(data: any, filename: string, normalizeFileName = false) { if (isObject(data)) { data = JSON.stringify(data, null, 2); } - this.downloadFile(data, filename, JSON_TYPE); + this.downloadFile(data, filename, JSON_TYPE, normalizeFileName); } - private downloadFile(data: any, filename: string, fileType: FileType) { - if (!filename) { - filename = 'download'; + private prepareFilename(filename: string, extension: string, normalizeFileName: boolean): string { + if (normalizeFileName) { + filename = filename.toLowerCase().replace(/\s/g, '_'); } - filename += '.' + fileType.extension; + filename = filename.replace(/[\\/<>:"|?*\s]/g, '_'); + return `${filename}.${extension}`; + } + + private downloadFile(data: any, filename = 'download', fileType: FileType, normalizeFileName = false) { + filename = this.prepareFilename(filename, fileType.extension, normalizeFileName); const blob = new Blob([data], {type: fileType.mimeType}); - // @ts-ignore - if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) { - // @ts-ignore - this.window.navigator.msSaveOrOpenBlob(blob, filename); - } else { - const e = this.document.createEvent('MouseEvents'); - const a = this.document.createElement('a'); - a.download = filename; - a.href = URL.createObjectURL(blob); - a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); - // @ts-ignore - e.initEvent('click', true, false, this.window, - 0, 0, 0, 0, 0, false, false, false, false, 0, null); - a.dispatchEvent(e); - } + const url = URL.createObjectURL(blob); + + const a = this.document.createElement('a'); + a.href = url; + a.download = filename; + a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 0); } private prepareDashboardExport(dashboard: Dashboard): Dashboard { From cdcb2c4fd68b395ea899277c73e70afa347a9e09 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 26 Sep 2025 11:59:10 +0300 Subject: [PATCH 2/2] UI: updated public api in import-export service --- .../import-export/import-export.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index d7c6d5880a..34e584a527 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -821,7 +821,7 @@ export class ImportExportService { return cellData; } - public exportCsv(data: {[key: string]: any}[], filename: string) { + public exportCsv(data: {[key: string]: any}[], filename: string, normalizeFileName = false) { let colsHead: string; let colsData: string; if (data && data.length) { @@ -836,18 +836,18 @@ export class ImportExportService { colsData = ''; } const csvData = `${colsHead}\n${colsData}`; - this.downloadFile(csvData, filename, CSV_TYPE); + this.downloadFile(csvData, filename, CSV_TYPE, normalizeFileName); } - public exportText(data: string | Array, filename: string) { + public exportText(data: string | Array, filename: string, normalizeFileName = false) { let content = data; if (Array.isArray(data)) { content = data.join('\n'); } - this.downloadFile(content, filename, TEXT_TYPE); + this.downloadFile(content, filename, TEXT_TYPE, normalizeFileName); } - public exportJSZip(data: object, filename: string): Observable { + public exportJSZip(data: object, filename: string, normalizeFileName = false): Observable { const exportJsSubjectSubject = new Subject(); import('jszip').then((JSZip) => { try { @@ -859,7 +859,7 @@ export class ImportExportService { } } jsZip.generateAsync({type: 'blob'}).then(content => { - this.downloadFile(content, filename, ZIP_TYPE); + this.downloadFile(content, filename, ZIP_TYPE, normalizeFileName); exportJsSubjectSubject.next(null); }).catch((e: any) => { exportJsSubjectSubject.error(e); @@ -1174,7 +1174,7 @@ export class ImportExportService { this.downloadFile(data, filename, JSON_TYPE, normalizeFileName); } - private prepareFilename(filename: string, extension: string, normalizeFileName: boolean): string { + private prepareFilename(filename: string, extension: string, normalizeFileName = false): string { if (normalizeFileName) { filename = filename.toLowerCase().replace(/\s/g, '_'); } @@ -1182,7 +1182,7 @@ export class ImportExportService { return `${filename}.${extension}`; } - private downloadFile(data: any, filename = 'download', fileType: FileType, normalizeFileName = false) { + private downloadFile(data: any, filename = 'download', fileType: FileType, normalizeFileName: boolean) { filename = this.prepareFilename(filename, fileType.extension, normalizeFileName); const blob = new Blob([data], {type: fileType.mimeType}); const url = URL.createObjectURL(blob);