UI: Add support UTF-8 symbols in export file name

This commit is contained in:
Vladyslav_Prykhodko 2025-09-26 11:37:15 +03:00
parent 6b4636ac48
commit fc9c692e0b

View File

@ -22,7 +22,6 @@ import { AppState } from '@core/core.state';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { BreakpointId, Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; import { BreakpointId, Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models';
import { deepClone, guid, isDefined, isNotEmptyStr, isObject, isString, isUndefined } from '@core/utils'; import { deepClone, guid, isDefined, isNotEmptyStr, isObject, isString, isUndefined } from '@core/utils';
import { WINDOW } from '@core/services/window.service';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { import {
AliasesInfo, AliasesInfo,
@ -100,8 +99,7 @@ type SupportEntityResources = 'includeResourcesInExportWidgetTypes' | 'includeRe
@Injectable() @Injectable()
export class ImportExportService { export class ImportExportService {
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(DOCUMENT) private document: Document,
@Inject(DOCUMENT) private document: Document,
private store: Store<AppState>, private store: Store<AppState>,
private translate: TranslateService, private translate: TranslateService,
private dashboardService: DashboardService, private dashboardService: DashboardService,
@ -177,9 +175,7 @@ export class ImportExportService {
public exportCalculatedField(calculatedFieldId: string): void { public exportCalculatedField(calculatedFieldId: string): void {
this.calculatedFieldsService.getCalculatedFieldById(calculatedFieldId).subscribe({ this.calculatedFieldsService.getCalculatedFieldById(calculatedFieldId).subscribe({
next: (calculatedField) => { next: (calculatedField) => {
let name = calculatedField.name; this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), calculatedField.name, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'calculated-fields.export-failed-error'); this.handleExportError(e, 'calculated-fields.export-failed-error');
@ -200,9 +196,7 @@ export class ImportExportService {
this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportDashboard'); this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportDashboard');
this.dashboardService.exportDashboard(dashboardId, result.include).subscribe({ this.dashboardService.exportDashboard(dashboardId, result.include).subscribe({
next: (dashboard) => { next: (dashboard) => {
let name = dashboard.title; this.exportToPc(this.prepareDashboardExport(dashboard), dashboard.title, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareDashboardExport(dashboard), name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'dashboard.export-failed-error'); this.handleExportError(e, 'dashboard.export-failed-error');
@ -261,9 +255,8 @@ export class ImportExportService {
widgetTitle: string, breakpoint: BreakpointId) { widgetTitle: string, breakpoint: BreakpointId) {
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint); const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName; const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName;
let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); const fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : '');
fileName = fileName.toLowerCase().replace(/\W/g, '_'); this.exportToPc(this.prepareExport(widgetItem), fileName, true);
this.exportToPc(this.prepareExport(widgetItem), fileName);
} }
public importWidget(dashboard: Dashboard, targetState: string, public importWidget(dashboard: Dashboard, targetState: string,
@ -360,9 +353,7 @@ export class ImportExportService {
this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportWidgetTypes'); this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportWidgetTypes');
this.widgetService.exportWidgetType(widgetTypeId, result.include).subscribe({ this.widgetService.exportWidgetType(widgetTypeId, result.include).subscribe({
next: (widgetTypeDetails) => { next: (widgetTypeDetails) => {
let name = widgetTypeDetails.name; this.exportToPc(this.prepareExport(widgetTypeDetails), widgetTypeDetails.name, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareExport(widgetTypeDetails), name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'widget-type.export-failed-error'); this.handleExportError(e, 'widget-type.export-failed-error');
@ -440,7 +431,7 @@ export class ImportExportService {
public exportEntity(entityData: VersionedEntity): void { public exportEntity(entityData: VersionedEntity): void {
const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId; const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId;
let fileName = (entityData as EntityInfoData).name; let fileName = (entityData as EntityInfoData).name;
let preparedData; let preparedData: any;
switch (id.entityType) { switch (id.entityType) {
case EntityType.DEVICE_PROFILE: case EntityType.DEVICE_PROFILE:
case EntityType.ASSET_PROFILE: case EntityType.ASSET_PROFILE:
@ -511,9 +502,7 @@ export class ImportExportService {
for (const widgetTypeDetails of widgetTypesDetails) { for (const widgetTypeDetails of widgetTypesDetails) {
widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails)); widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails));
} }
let name = widgetsBundle.title; this.exportToPc(widgetsBundleItem, widgetsBundle.title, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(widgetsBundleItem, name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error'); this.handleExportError(e, 'widgets-bundle.export-failed-error');
@ -528,9 +517,7 @@ export class ImportExportService {
widgetsBundle: this.prepareExport(widgetsBundle), widgetsBundle: this.prepareExport(widgetsBundle),
widgetTypeFqns widgetTypeFqns
}; };
let name = widgetsBundle.title; this.exportToPc(widgetsBundleItem, widgetsBundle.title, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(widgetsBundleItem, name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error'); this.handleExportError(e, 'widgets-bundle.export-failed-error');
@ -662,11 +649,9 @@ export class ImportExportService {
private onRuleChainExported() { private onRuleChainExported() {
return { return {
next: (ruleChainExport: RuleChainImport) => { next: (ruleChainExport: RuleChainImport) => {
let name = ruleChainExport.ruleChain.name; this.exportToPc(ruleChainExport, ruleChainExport.ruleChain.name, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(ruleChainExport, name);
}, },
error: (e) => { error: (e: any) => {
this.handleExportError(e, 'rulechain.export-failed-error'); this.handleExportError(e, 'rulechain.export-failed-error');
} }
}; };
@ -747,9 +732,7 @@ export class ImportExportService {
public exportDeviceProfile(deviceProfileId: string) { public exportDeviceProfile(deviceProfileId: string) {
this.deviceProfileService.exportDeviceProfile(deviceProfileId).subscribe({ this.deviceProfileService.exportDeviceProfile(deviceProfileId).subscribe({
next: (deviceProfile) => { next: (deviceProfile) => {
let name = deviceProfile.name; this.exportToPc(this.prepareProfileExport(deviceProfile), deviceProfile.name, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareProfileExport(deviceProfile), name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'device-profile.export-failed-error'); this.handleExportError(e, 'device-profile.export-failed-error');
@ -776,9 +759,7 @@ export class ImportExportService {
public exportAssetProfile(assetProfileId: string) { public exportAssetProfile(assetProfileId: string) {
this.assetProfileService.exportAssetProfile(assetProfileId).subscribe({ this.assetProfileService.exportAssetProfile(assetProfileId).subscribe({
next: (assetProfile) => { next: (assetProfile) => {
let name = assetProfile.name; this.exportToPc(this.prepareProfileExport(assetProfile), assetProfile.name, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareProfileExport(assetProfile), name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'asset-profile.export-failed-error'); this.handleExportError(e, 'asset-profile.export-failed-error');
@ -805,9 +786,7 @@ export class ImportExportService {
public exportTenantProfile(tenantProfileId: string) { public exportTenantProfile(tenantProfileId: string) {
this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe({ this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe({
next: (tenantProfile) => { next: (tenantProfile) => {
let name = tenantProfile.name; this.exportToPc(this.prepareProfileExport(tenantProfile), tenantProfile.name, true);
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareProfileExport(tenantProfile), name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'tenant-profile.export-failed-error'); this.handleExportError(e, 'tenant-profile.export-failed-error');
@ -882,7 +861,7 @@ export class ImportExportService {
jsZip.generateAsync({type: 'blob'}).then(content => { jsZip.generateAsync({type: 'blob'}).then(content => {
this.downloadFile(content, filename, ZIP_TYPE); this.downloadFile(content, filename, ZIP_TYPE);
exportJsSubjectSubject.next(null); exportJsSubjectSubject.next(null);
}).catch(e => { }).catch((e: any) => {
exportJsSubjectSubject.error(e); exportJsSubjectSubject.error(e);
}); });
} catch (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) { if (!data) {
console.error('No data'); console.error('No data');
return; 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)) { if (isObject(data)) {
data = JSON.stringify(data, null, 2); 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) { private prepareFilename(filename: string, extension: string, normalizeFileName: boolean): string {
if (!filename) { if (normalizeFileName) {
filename = 'download'; 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}); const blob = new Blob([data], {type: fileType.mimeType});
// @ts-ignore const url = URL.createObjectURL(blob);
if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) {
// @ts-ignore const a = this.document.createElement('a');
this.window.navigator.msSaveOrOpenBlob(blob, filename); a.href = url;
} else { a.download = filename;
const e = this.document.createEvent('MouseEvents'); a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':');
const a = this.document.createElement('a'); a.click();
a.download = filename; setTimeout(() => URL.revokeObjectURL(url), 0);
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);
}
} }
private prepareDashboardExport(dashboard: Dashboard): Dashboard { private prepareDashboardExport(dashboard: Dashboard): Dashboard {