@xiriframework/xiri-ng 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2904,7 +2904,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
2904
2904
  }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }] } });
2905
2905
 
2906
2906
  class XiriDownloadService {
2907
- download(result, filename, open) {
2907
+ /**
2908
+ * Opens an empty tab and returns its handle for a later download( …, tab ).
2909
+ *
2910
+ * MUST be called synchronously from the click handler: after an HTTP round trip the
2911
+ * transient user activation is gone and the browser blocks the popup. Returns null
2912
+ * when it was blocked anyway — pass that through, download() then saves the file.
2913
+ */
2914
+ openTab() {
2915
+ return window.open('about:blank', '_blank');
2916
+ }
2917
+ /**
2918
+ * Turns the response body into a file. With a tab handle from openTab() the file is
2919
+ * displayed in that tab, otherwise it is saved via a download anchor.
2920
+ */
2921
+ download(result, filename, tab) {
2908
2922
  const contentDisposition = result.headers.get('content-disposition');
2909
2923
  if (contentDisposition) {
2910
2924
  filename = contentDisposition.split('filename=')[1];
@@ -2913,9 +2927,14 @@ class XiriDownloadService {
2913
2927
  const contentType = result.headers.get('content-type') ?? undefined;
2914
2928
  const file = new File([result.body], filename, { type: contentType });
2915
2929
  const fileData = URL.createObjectURL(file);
2916
- if (open) {
2917
- const ret = window.open(fileData, '_blank');
2918
- return !(ret === null || typeof (ret) == 'undefined');
2930
+ if (tab) {
2931
+ // ponytail: caps window.opener reach. A blob: document is same-origin anyway, so this is
2932
+ // no licence to display untrusted HTML only a correct Content-Type keeps it a viewer.
2933
+ tab.opener = null;
2934
+ tab.location.replace(fileData);
2935
+ // ponytail: 60s is plenty to load; without it the blob lives until the page reloads.
2936
+ setTimeout(() => URL.revokeObjectURL(fileData), 60 * 1000);
2937
+ return true;
2919
2938
  }
2920
2939
  const a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
2921
2940
  a.download = filename;
@@ -3162,7 +3181,7 @@ class XiriDialogComponent {
3162
3181
  });
3163
3182
  }, this.refreshTime);
3164
3183
  }
3165
- download(data) {
3184
+ download(data, tab) {
3166
3185
  if (data === null) {
3167
3186
  // TODO: fix, this is actually an external link
3168
3187
  const file = window.open(this.dataService.getConfigApi() + this.url, 'Report');
@@ -3182,7 +3201,7 @@ class XiriDialogComponent {
3182
3201
  .pipe(takeUntilDestroyed(this.destroyRef))
3183
3202
  .subscribe({
3184
3203
  next: (result) => {
3185
- const ret = this.downloadService.download(result, 'Report', false);
3204
+ const ret = this.downloadService.download(result, 'Report', tab);
3186
3205
  if (!ret)
3187
3206
  this.buttons.set([{
3188
3207
  text: 'Download',
@@ -3194,6 +3213,7 @@ class XiriDialogComponent {
3194
3213
  else
3195
3214
  this.dialogRef.close(this.extra);
3196
3215
  }, error: (err) => {
3216
+ tab?.close();
3197
3217
  this.snackbar.error(err.error?.error || 'Unknown Error');
3198
3218
  }
3199
3219
  });
@@ -3217,7 +3237,10 @@ class XiriDialogComponent {
3217
3237
  this.startSend(button.data);
3218
3238
  }
3219
3239
  else if (button.action == 'download') {
3220
- this.download(this.type() == 'form' ? this.formValues : null);
3240
+ const data = this.type() == 'form' ? this.formValues : null;
3241
+ // Only the blob path needs a tab handle; data === null is an external link that
3242
+ // opens its own tab. openTab() must run here, inside the click.
3243
+ this.download(data, data !== null && button.target === '_blank' ? this.downloadService.openTab() : null);
3221
3244
  }
3222
3245
  else if (button.action == 'link') {
3223
3246
  this.router.navigate([button.url]).then();
@@ -3545,6 +3568,9 @@ class XiriButtonComponent {
3545
3568
  actionDownload() {
3546
3569
  const data = { ...this.filterData(), ...this.button().data };
3547
3570
  this.loading.set(true);
3571
+ // Must happen before the request: openTab() needs the click's user activation.
3572
+ // Without one (e.g. autoLoad) it returns null and the file is saved instead.
3573
+ const tab = this.button().target === '_blank' ? this.downloadService.openTab() : null;
3548
3574
  this.dataService.postFileResponse(this._url() ?? '', data)
3549
3575
  .pipe(takeUntilDestroyed(this.destroyRef))
3550
3576
  .subscribe({
@@ -3554,9 +3580,11 @@ class XiriButtonComponent {
3554
3580
  if (btn.filename != undefined)
3555
3581
  filename = btn.filename;
3556
3582
  else
3557
- filename =
3558
- (btn.text != undefined ? btn.text : 'Download') + '.csv';
3559
- this.downloadService.download(result, filename, false);
3583
+ // ponytail: no extension guess when the file is displayed — the
3584
+ // viewer would offer "Report.csv" for a PDF on save.
3585
+ filename = (btn.text != undefined ? btn.text : 'Download')
3586
+ + (tab ? '' : '.csv');
3587
+ this.downloadService.download(result, filename, tab);
3560
3588
  this.result.emit({
3561
3589
  button: this.button(),
3562
3590
  result: result,
@@ -3567,6 +3595,7 @@ class XiriButtonComponent {
3567
3595
  },
3568
3596
  error: (err) => {
3569
3597
  console.log("xiri-button download error", err);
3598
+ tab?.close();
3570
3599
  this.result.emit({
3571
3600
  button: this.button(),
3572
3601
  result: null,
@@ -4906,12 +4935,18 @@ class XiriTableComponent {
4906
4935
  });
4907
4936
  break;
4908
4937
  }
4909
- case 'download':
4938
+ case 'download': {
4939
+ // Before the request — openTab() needs the click's user activation.
4940
+ const tab = button.target === '_blank' ? this.downloadService.openTab() : null;
4910
4941
  this.dataService.postFileResponse(button.url ?? '', payload).pipe(takeUntil(this.reloadAbort$)).subscribe({
4911
- next: (result) => this.downloadService.download(result, button.filename || 'download.csv', false),
4912
- error: (err) => this.snackbar.error(errorMessage(err) || 'Download Error'),
4942
+ next: (result) => this.downloadService.download(result, button.filename || 'download.csv', tab),
4943
+ error: (err) => {
4944
+ tab?.close();
4945
+ this.snackbar.error(errorMessage(err) || 'Download Error');
4946
+ },
4913
4947
  });
4914
4948
  break;
4949
+ }
4915
4950
  default:
4916
4951
  this.dataService.post(button.url ?? '', payload).pipe(takeUntil(this.reloadAbort$)).subscribe({
4917
4952
  next: (result) => {
@@ -5003,11 +5038,14 @@ class XiriTableComponent {
5003
5038
  if (this.selection.isEmpty())
5004
5039
  return;
5005
5040
  const url = button.url ? button.url : '';
5041
+ // Before the request — openTab() needs the click's user activation.
5042
+ const tab = button.target === '_blank' ? this.downloadService.openTab() : null;
5006
5043
  this.dataService.postFileResponse(url, this.getSelectionIDs()).pipe(takeUntil(this.reloadAbort$)).subscribe({
5007
5044
  next: (result) => {
5008
- this.downloadService.download(result, 'download.csv', false);
5045
+ this.downloadService.download(result, 'download.csv', tab);
5009
5046
  },
5010
5047
  error: (err) => {
5048
+ tab?.close();
5011
5049
  this.snackbar.error(errorMessage(err) || 'Download Error');
5012
5050
  }
5013
5051
  });