@firestitch/report 18.17.0 → 18.17.2

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.
@@ -50,7 +50,7 @@ import * as i1$5 from '@angular/cdk/drag-drop';
50
50
  import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
51
51
  import * as i3$1 from '@firestitch/zoom-pan';
52
52
  import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
53
- import { GridFieldType, GridGranularity, canonicalGridLayout, convertDateFilter, GridNoteCode, GridControlState, GridFormat, GridUnits, FsGridComponent, FsGridCellDirective, GridRowKind, readGridTable } from '@firestitch/grid';
53
+ import { GridFieldType, GridGranularity, canonicalGridLayout, convertDateFilter, GridNoteCode, GridControlState, GridFormat, GridUnits, FsGridComponent, FsGridCellDirective, GridRowKind, headingLabel, readGridTable } from '@firestitch/grid';
54
54
  import { parseISO, isValid, addDays, min, max, startOfDay, isLastDayOfMonth, endOfMonth, addMonths, addYears, addQuarters, addWeeks, formatISO, format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek } from 'date-fns';
55
55
  import { toZonedTime } from 'date-fns-tz';
56
56
  import { MatProgressSpinner } from '@angular/material/progress-spinner';
@@ -429,14 +429,14 @@ class ReportData {
429
429
  }
430
430
  // Make one column filterable. `level` (component | report) places a new
431
431
  // group; `groupId` instead links into an existing group. A select filter's
432
- // options are derived from the component SQL when optionsSql is omitted.
432
+ // options are derived from the component's own query.
433
433
  addFilter(reportId, componentId, filter, config = {}) {
434
434
  return this._api.post(this._path(`${reportId}/components/${componentId}/filters`), filter, {
435
435
  key: 'filter',
436
436
  ...config,
437
437
  });
438
438
  }
439
- // Update a filter — any subset of { label, optionsSql, enabled }.
439
+ // Update a filter — any subset of { label, enabled }.
440
440
  updateFilter(reportId, filterId, changes, config = {}) {
441
441
  return this._api.put(this._path(`${reportId}/filters/${filterId}`), changes, {
442
442
  key: 'filter',
@@ -9743,9 +9743,10 @@ const NO = 'No';
9743
9743
  // A grid tile's formatter: every value written as the report writes it on
9744
9744
  // a list today. An output number reads through formatOutputValue with what
9745
9745
  // the report line says the output holds (a change is signed, a missing prior
9746
- // reads as the list's dash); a plain number through formatNumber; a date in
9747
- // the report's locale, an instant in its zone. The placement's format,
9748
- // precision and units win over the field's.
9746
+ // reads as the list's dash); a plain number through formatNumber; a key's
9747
+ // digits as they are; a date at its grain in the report's locale, an instant
9748
+ // in its zone. The placement's format, precision and units win over the
9749
+ // field's.
9749
9750
  function reportGridFormatter(context) {
9750
9751
  return {
9751
9752
  format: (value, field, placement) => formatGridValue(context, value, field, placement),
@@ -9760,6 +9761,7 @@ function formatGridValue(context, value, field, placement) {
9760
9761
  case GridFieldType.Number:
9761
9762
  return formatGridNumber(context, value, field, placement);
9762
9763
  case GridFieldType.Date:
9764
+ return formatGridDate(context, value, field, placement);
9763
9765
  case GridFieldType.Datetime:
9764
9766
  return formatReportDate(value, context.timezone, context.format.locale);
9765
9767
  case GridFieldType.Boolean:
@@ -9769,12 +9771,29 @@ function formatGridValue(context, value, field, placement) {
9769
9771
  }
9770
9772
  }
9771
9773
  function formatGridNumber(context, value, field, placement) {
9774
+ // A key, such as a client's id, is a name: its digits print as they are, never grouped
9775
+ if (field.identifier && !isAggregate(placement)) {
9776
+ return String(value);
9777
+ }
9772
9778
  const spec = numberSpec(field, placement);
9773
9779
  const output = isAggregate(placement) ? null : context.report?.outputs?.[field.name];
9774
9780
  return output
9775
9781
  ? formatOutputValue(value, output, spec, context.format).text
9776
9782
  : formatNumber(value, spec, context.format);
9777
9783
  }
9784
+ // A calendar date at the grain it prints at: the placement's, else the
9785
+ // field's own, which on a time axis is the report's group-by. A quarter reads
9786
+ // "Q3 2023", a month "Jul 2023" and a year "2023", as the report's charts
9787
+ // label them, rather than as their first day; a week, like a chart's, reads as
9788
+ // the day it starts. A day reads as the report writes a date.
9789
+ function formatGridDate(context, value, field, placement) {
9790
+ const granularity = placement?.granularity ?? field.granularities?.[0];
9791
+ const chartGranularity = Object.keys(GRID_GRANULARITIES)
9792
+ .find((name) => GRID_GRANULARITIES[name] === granularity);
9793
+ return chartGranularity && chartGranularity !== 'day' && typeof value === 'string'
9794
+ ? formatPeriodLabel(value.slice(0, 10), chartGranularity, false, context.format.locale, ReportChartDateLabel.Long)
9795
+ : formatReportDate(value, context.timezone, context.format.locale);
9796
+ }
9778
9797
  // The format, precision and units a number is written with: the placement's,
9779
9798
  // else the field's.
9780
9799
  function numberSpec(field, placement) {
@@ -14700,11 +14719,12 @@ function gridExportTable(table) {
14700
14719
  };
14701
14720
  }
14702
14721
  // The lines a table adds under its box: the screen's notes, the pivot
14703
- // columns it leaves out, and each column filter of the layout it was read in.
14722
+ // columns it leaves out, and each column filter of the layout it was read in,
14723
+ // its field named as its column is headed.
14704
14724
  function gridExportNotes(table, layout, locale) {
14705
14725
  const fields = new Map(table.fields.map((field) => [field.name, field]));
14706
14726
  const filters = (layout.filters ?? [])
14707
- .map((filter) => gridFilterLineText(fields.get(filter.field)?.label ?? filter.field, filter, fields.get(filter.field), locale))
14727
+ .map((filter) => gridFilterLineText(headingLabel(table.fields, layout, filter.field), filter, fields.get(filter.field), locale))
14708
14728
  .filter((text) => !!text);
14709
14729
  return [
14710
14730
  ...table.notes,
@@ -15391,7 +15411,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
15391
15411
  // in the report's format context, as on screen.
15392
15412
  async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
15393
15413
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
15394
- const echarts = (await import('./firestitch-report-echarts-7AUAX6RO.mjs')).default;
15414
+ const echarts = (await import('./firestitch-report-echarts-CBoGhanh.mjs')).default;
15395
15415
  const host = document.createElement('div');
15396
15416
  host.style.width = `${width}px`;
15397
15417
  host.style.height = `${height}px`;
@@ -15702,6 +15722,18 @@ const NO_BAND_MARGIN = 0.5 * PT;
15702
15722
  const NO_BANDS = { headerHeight: 0, footerHeight: 0 };
15703
15723
  // The last row of a list box whose rows carry on past it.
15704
15724
  const CONTINUED_NOTICE = 'Continued on the following pages';
15725
+ // The last row of a list box whose rows have nowhere to go: its heading and
15726
+ // totals row, wrapped tall in narrow columns, leave a continuation page no
15727
+ // room for a row.
15728
+ const NO_ROOM_NOTICE = 'Not enough room for this table — enlarge the tile';
15729
+ // A list's or a table tile's table: its font size and cell padding, points.
15730
+ const TABLE_FONT_SIZE = 8;
15731
+ const TABLE_CELL_PADDING = 3;
15732
+ // What a component's box says when it could not be drawn.
15733
+ const UNDRAWN_NOTE = 'Could not be drawn';
15734
+ // Thrown from autoTable's page hook to stop an on-page table at the first page
15735
+ // it would add (_drawTable).
15736
+ const STRAY_PAGE = new Error('The table ran past its page');
15705
15737
  // A text line's height as a multiple of its largest font size — the canvas
15706
15738
  // stylesheet's line-height, so a block wraps and stacks the same way.
15707
15739
  const TEXT_LINE_HEIGHT = 1.25;
@@ -15724,7 +15756,14 @@ const GRID_INDENT$1 = 8;
15724
15756
  // row-height estimate, so it cannot spill past the box. The rows that do not
15725
15757
  // fit continue on pages of their own after the report page, under the same
15726
15758
  // header/footer and with the same column widths, up to PDF_LIST_ROWS; a
15727
- // longer result ends in a row pointing to the component's CSV export.
15759
+ // longer result ends in a row pointing to the component's CSV export. A
15760
+ // table whose heading and totals row leave a continuation page no room for a
15761
+ // row stops at its box with a notice instead: autoTable would add pages
15762
+ // without end trying to place it.
15763
+ //
15764
+ // A component that fails to draw is marked in its box and the file goes on
15765
+ // (_guarded); build() hands back the ones that did, as it does the text it
15766
+ // cut.
15728
15767
  //
15729
15768
  // A text block's tables are drawn by autoTable too, from the line its words
15730
15769
  // reach, in the column widths the canvas gives them (ReportTextLayoutService).
@@ -15746,12 +15785,16 @@ class ReportPdfService {
15746
15785
  // Each continuation appends its pages at the end of the document, so
15747
15786
  // two lists that overflowed on one page follow it back to back.
15748
15787
  for (const overflow of overflows) {
15749
- pageNumber = await this._renderContinuation(context, collectedPage.page, index, pageNumber, overflow);
15788
+ const tokens = this._tokens(context, collectedPage.page, index, pageNumber + 1);
15789
+ await this._guarded(context, overflow.component, tokens.pageName, tokens, false, async () => {
15790
+ pageNumber = await this._renderContinuation(context, collectedPage.page, index, pageNumber, overflow);
15791
+ });
15750
15792
  }
15751
15793
  }
15752
15794
  return {
15753
15795
  overflows: [...context.textOverflows.values()],
15754
15796
  overflowNote: PDF_OVERFLOW_NOTE,
15797
+ undrawn: [...context.undrawn.values()],
15755
15798
  save: () => {
15756
15799
  context.doc.save(`${this._slug(collected.report.name)}.pdf`);
15757
15800
  },
@@ -15760,13 +15803,14 @@ class ReportPdfService {
15760
15803
  async _context(collected) {
15761
15804
  const report = collected.report;
15762
15805
  const { jsPDF } = await import('jspdf');
15763
- const autoTable = (await import('jspdf-autotable')).default;
15806
+ const { default: autoTable, __createTable: createTable } = await import('jspdf-autotable');
15764
15807
  const pageW = report.pageWidth * PT;
15765
15808
  const pageH = report.pageHeight * PT;
15766
15809
  const orientation = pageW >= pageH ? 'landscape' : 'portrait';
15767
15810
  return {
15768
15811
  doc: new jsPDF({ orientation, unit: 'pt', format: [pageW, pageH] }),
15769
15812
  autoTable,
15813
+ createTable,
15770
15814
  jsPDF,
15771
15815
  report,
15772
15816
  timezone: resolveReportTimezone(report),
@@ -15778,6 +15822,7 @@ class ReportPdfService {
15778
15822
  filterFields: collected.filterFields,
15779
15823
  masterFilters: layerFilterFields(collected.filterFields, report.filterGroups ?? [], null),
15780
15824
  textOverflows: new Map(),
15825
+ undrawn: new Map(),
15781
15826
  };
15782
15827
  }
15783
15828
  // One report page: the header and footer first, under the page's own
@@ -15790,14 +15835,59 @@ class ReportPdfService {
15790
15835
  doc.addPage([context.pageW, context.pageH], context.orientation);
15791
15836
  await this._renderMaster(context, collectedPage.page, tokens);
15792
15837
  for (const item of collectedPage.components) {
15793
- if (item.component.type !== 'text') {
15794
- await this._renderComponent(context, item, tokens, overflows);
15838
+ await this._guarded(context, item.component, tokens.pageName, pageTokens, true, async () => {
15839
+ if (item.component.type !== 'text') {
15840
+ await this._renderComponent(context, collectedPage.page, item, tokens, overflows);
15841
+ }
15842
+ else if (await this._renderText(context, item.component, pageTokens)) {
15843
+ this._recordOverflow(context, item.component, tokens.pageName, pageTokens);
15844
+ }
15845
+ });
15846
+ }
15847
+ }
15848
+ // Draws one component, or one list's continuation pages, so that whatever
15849
+ // goes wrong stays with it: a draw that throws loses the pages it added, its
15850
+ // box on the page (boxed) is blanked under UNDRAWN_NOTE, and the export's
15851
+ // gate names it under page. One component can then never stop the file.
15852
+ async _guarded(context, component, page, tokens, boxed, draw) {
15853
+ const { doc } = context;
15854
+ const current = doc.getCurrentPageInfo().pageNumber;
15855
+ const pages = doc.getNumberOfPages();
15856
+ try {
15857
+ await draw();
15858
+ }
15859
+ catch (error) {
15860
+ // The file only says the box could not be drawn; the console keeps why.
15861
+ console.error(error);
15862
+ while (doc.getNumberOfPages() > pages) {
15863
+ doc.deletePage(doc.getNumberOfPages());
15795
15864
  }
15796
- else if (await this._renderText(context, item.component, pageTokens)) {
15797
- this._recordOverflow(context, item.component, tokens.pageName, pageTokens);
15865
+ doc.setPage(current);
15866
+ if (boxed) {
15867
+ this._undrawnBox(doc, component);
15868
+ }
15869
+ if (!context.undrawn.has(component.id)) {
15870
+ context.undrawn.set(component.id, {
15871
+ componentId: component.id,
15872
+ page,
15873
+ name: component.type === 'text'
15874
+ ? textOverflow(component, page, tokens).snippet
15875
+ : exportTitle(component, tokens),
15876
+ });
15798
15877
  }
15799
15878
  }
15800
15879
  }
15880
+ // A component's box blanked under UNDRAWN_NOTE, over whatever of it was
15881
+ // drawn before it failed.
15882
+ _undrawnBox(doc, component) {
15883
+ const x = component.x * PT;
15884
+ const y = component.y * PT;
15885
+ const w = component.w * PT;
15886
+ const h = component.h * PT;
15887
+ doc.setFillColor('#ffffff');
15888
+ doc.rect(x, y, w, h, 'F');
15889
+ this._boxNote(doc, UNDRAWN_NOTE, x, y, w, h);
15890
+ }
15801
15891
  // The header and footer, their value tokens filled with the master's values
15802
15892
  // whichever page they sit on: a report page or a continuation page. They
15803
15893
  // read no page filter: its token prints the em dash there.
@@ -15807,9 +15897,11 @@ class ReportPdfService {
15807
15897
  }
15808
15898
  const masterTokens = { ...tokens, values: context.masterValues, filters: context.masterFilters };
15809
15899
  for (const component of (context.report.master?.components ?? []).filter(isDrawn)) {
15810
- if (await this._renderText(context, component, masterTokens)) {
15811
- this._recordOverflow(context, component, HEADER_FOOTER_NAME, masterTokens);
15812
- }
15900
+ await this._guarded(context, component, HEADER_FOOTER_NAME, masterTokens, true, async () => {
15901
+ if (await this._renderText(context, component, masterTokens)) {
15902
+ this._recordOverflow(context, component, HEADER_FOOTER_NAME, masterTokens);
15903
+ }
15904
+ });
15813
15905
  }
15814
15906
  }
15815
15907
  // Lists a text block that was cut, under the page the export's overflow gate
@@ -15935,7 +16027,8 @@ class ReportPdfService {
15935
16027
  // wraps them, and the words are drawn into each cell once autoTable has
15936
16028
  // drawn its grid lines and fill (_drawTextCell), so they keep their runs'
15937
16029
  // fonts and colors. Only the rows the box holds are drawn, and a table that
15938
- // loses any is cut. The cursor it leaves is TEXT_TABLE_MARGIN under it.
16030
+ // loses any, or runs past its page anyway, is cut. The cursor it leaves is
16031
+ // TEXT_TABLE_MARGIN under it.
15939
16032
  _drawTextTable(context, table, layout, box, top) {
15940
16033
  if (top >= box.bottom) {
15941
16034
  return { cursorY: top, cut: true };
@@ -15947,13 +16040,11 @@ class ReportPdfService {
15947
16040
  : Array.from({ length: table.columns }, () => width / table.columns);
15948
16041
  const body = table.rows.map((row) => row.cells.map((cell) => this._textCellInput(context.doc, cell, columnWidths)));
15949
16042
  const options = this._textTableOptions(context, columnWidths, left, top, width, box.bottom);
15950
- const fit = this._textTableFit(context, { ...options, body });
15951
- if (fit) {
15952
- this._drawTable(context, { ...options, body: this._keptRows(body, fit) });
15953
- }
16043
+ const fit = this._textTableFit(context, { ...options, body }, top, box.bottom);
16044
+ const whole = !fit || this._drawTable(context, { ...options, body: this._keptRows(body, fit) });
15954
16045
  return {
15955
- cursorY: fit ? context.doc.lastAutoTable.finalY + TEXT_TABLE_MARGIN : top,
15956
- cut: fit < table.rows.length,
16046
+ cursorY: fit && whole ? context.doc.lastAutoTable.finalY + TEXT_TABLE_MARGIN : top,
16047
+ cut: !whole || fit < table.rows.length,
15957
16048
  };
15958
16049
  }
15959
16050
  // A text table's cell as autoTable takes it: its spans and fill and, when it
@@ -16007,22 +16098,10 @@ class ReportPdfService {
16007
16098
  didDrawCell: (hook) => this._drawTextCell(hook),
16008
16099
  };
16009
16100
  }
16010
- // How many of a text table's rows its box holds: the rows autoTable draws
16011
- // whole on the first page when it lays the table out on a scratch document
16012
- // of the same page size, as _measure does for a list. A row taller than a
16013
- // page is split across pages rather than moved, so it does not count.
16014
- _textTableFit(context, options) {
16015
- const scratch = this._scratchDocument(context);
16016
- let fit = 0;
16017
- context.autoTable(scratch, {
16018
- ...options,
16019
- didDrawCell: (hook) => {
16020
- if (scratch.getCurrentPageInfo().pageNumber === 1 && !hook.row.spansMultiplePages) {
16021
- fit = Math.max(fit, hook.row.index + 1);
16022
- }
16023
- },
16024
- });
16025
- return fit;
16101
+ // How many of a text table's rows its box, from top to bottom, holds whole:
16102
+ // the table laid out as _measure lays out a list's.
16103
+ _textTableFit(context, options, top, bottom) {
16104
+ return this._fittingRows(context.createTable(this._scratchDocument(context), options), top, bottom, 0);
16026
16105
  }
16027
16106
  // The first `fit` rows, a cell that spans down past the last of them cut
16028
16107
  // there.
@@ -16146,7 +16225,7 @@ class ReportPdfService {
16146
16225
  };
16147
16226
  }
16148
16227
  // A tile on its report page, its title's tokens filled with that page's.
16149
- async _renderComponent(context, item, tokens, overflows) {
16228
+ async _renderComponent(context, page, item, tokens, overflows) {
16150
16229
  const { doc } = context;
16151
16230
  const component = item.component;
16152
16231
  // Geometry is stored in inches; the PDF draws in points.
@@ -16187,7 +16266,7 @@ class ReportPdfService {
16187
16266
  }
16188
16267
  case 'list':
16189
16268
  case 'grid':
16190
- this._renderTable(context, item, x, bodyY, w, bodyH, overflows);
16269
+ this._renderTable(context, page, item, x, bodyY, w, bodyH, overflows);
16191
16270
  break;
16192
16271
  case 'kpi':
16193
16272
  this._renderKpi(context, item, x, bodyY, w, bodyH);
@@ -16225,42 +16304,51 @@ class ReportPdfService {
16225
16304
  doc.text(this._pdfText(title), x, y + 10, { maxWidth: w });
16226
16305
  }
16227
16306
  // An italic grey line centered in a component's body, for a body that holds
16228
- // something other than its content.
16307
+ // something other than its content. A line wider than the body wraps.
16229
16308
  _boxNote(doc, text, x, y, w, h) {
16230
16309
  doc.setFont('helvetica', 'italic');
16231
16310
  doc.setFontSize(9);
16232
16311
  doc.setTextColor(MUTED_COLOR$1);
16233
- doc.text(text, x + w / 2, y + h / 2, { align: 'center' });
16312
+ doc.text(text, x + w / 2, y + h / 2, { align: 'center', maxWidth: w });
16234
16313
  }
16235
16314
  // A list's on-page table. All of its fetched rows are laid out once to learn
16236
16315
  // how many the box holds; when they do not all fit, the box's last row says
16237
16316
  // the table continues and the rest go to overflows for continuation pages.
16238
16317
  // A box too short for even the head carries the notice as a line instead.
16239
- // A totals row closes the table once: in the box when every row fits it,
16240
- // else after the last continuation row. Points throughout.
16241
- _renderTable(context, item, x, y, w, h, overflows) {
16318
+ // A table whose heading and totals row leave a continuation page no room for
16319
+ // a row stops at its box, its notice saying so. A totals row closes the
16320
+ // table once: in the box when every row fits it, else after the last
16321
+ // continuation row. Points throughout.
16322
+ _renderTable(context, page, item, x, y, w, h, overflows) {
16242
16323
  const { component, search, filtered } = item;
16243
16324
  const { head, cells, numeric, foot, omitted } = item.table ? this._gridCells(item) : this._listCells(context, item);
16244
16325
  const bottom = context.pageH - (y + h);
16245
- const { fit, columnStyles } = this._measure(context, head, cells, numeric, foot, x, y, w, bottom);
16326
+ const { fit, columnStyles, minHeight } = this._measure(context, head, cells, numeric, foot, x, y, w, bottom);
16246
16327
  if (fit === cells.length && omitted === 0) {
16247
16328
  this._drawTable(context, this._tableOptions(context, head, numeric, cells, x, y, w, bottom, columnStyles, foot));
16248
16329
  return;
16249
16330
  }
16331
+ // autoTable needs room for a row under the heading on every page it adds,
16332
+ // or it adds pages until the browser's stack runs out.
16333
+ const continues = minHeight < this._continuationRoom(context, page);
16334
+ const notice = continues ? CONTINUED_NOTICE : NO_ROOM_NOTICE;
16250
16335
  // The notice takes the place of the last row that fit, so that row moves
16251
16336
  // to the continuation. omitted > 0 means a full PDF_LIST_ROWS fetch, which
16252
16337
  // never fits one box, so the CSV notice always lands on a continuation
16253
16338
  // page.
16254
16339
  const kept = Math.max(0, fit - 1);
16255
- overflows.push({
16256
- component, search, crumbs: item.crumbs, filtered, head, numeric, rows: cells.slice(kept), omitted, foot, x, w, columnStyles,
16257
- });
16258
16340
  if (fit >= 1) {
16259
- const body = [...cells.slice(0, kept), this._noticeRow(CONTINUED_NOTICE, numeric.length, true)];
16341
+ const body = [...cells.slice(0, kept), this._noticeRow(notice, numeric.length, true)];
16260
16342
  this._drawTable(context, this._tableOptions(context, head, numeric, body, x, y, w, bottom, columnStyles));
16261
16343
  }
16262
16344
  else {
16263
- this._boxNote(context.doc, CONTINUED_NOTICE, x, y, w, h);
16345
+ this._boxNote(context.doc, notice, x, y, w, h);
16346
+ }
16347
+ if (continues) {
16348
+ overflows.push({
16349
+ component, search, crumbs: item.crumbs, filtered, head, numeric, rows: cells.slice(kept), omitted, foot, x, w,
16350
+ columnStyles,
16351
+ });
16264
16352
  }
16265
16353
  }
16266
16354
  // A list's heading, rows, totals row and the rows past its fetch.
@@ -16282,7 +16370,16 @@ class ReportPdfService {
16282
16370
  rowSpan: exported.rowSpan,
16283
16371
  styles: {
16284
16372
  ...(exported.bold ? { fontStyle: 'bold' } : {}),
16285
- ...(exported.indent ? { cellPadding: { top: 3, right: 3, bottom: 3, left: 3 + exported.indent * GRID_INDENT$1 } } : {}),
16373
+ ...(exported.indent
16374
+ ? {
16375
+ cellPadding: {
16376
+ top: TABLE_CELL_PADDING,
16377
+ right: TABLE_CELL_PADDING,
16378
+ bottom: TABLE_CELL_PADDING,
16379
+ left: TABLE_CELL_PADDING + exported.indent * GRID_INDENT$1,
16380
+ },
16381
+ }
16382
+ : {}),
16286
16383
  },
16287
16384
  });
16288
16385
  return {
@@ -16325,7 +16422,13 @@ class ReportPdfService {
16325
16422
  margin: { left: x, right: context.pageW - x - w, bottom },
16326
16423
  tableWidth: w,
16327
16424
  rowPageBreak: 'avoid',
16328
- styles: { font: 'helvetica', fontStyle: 'normal', fontSize: 8, cellPadding: 3, textColor: '#1f2933' },
16425
+ styles: {
16426
+ font: 'helvetica',
16427
+ fontStyle: 'normal',
16428
+ fontSize: TABLE_FONT_SIZE,
16429
+ cellPadding: TABLE_CELL_PADDING,
16430
+ textColor: '#1f2933',
16431
+ },
16329
16432
  headStyles: { fillColor: '#4e79a7', textColor: '#ffffff', fontStyle: 'bold' },
16330
16433
  alternateRowStyles: { fillColor: '#f5f7fa' },
16331
16434
  ...(columnStyles ? { columnStyles } : {}),
@@ -16363,37 +16466,51 @@ class ReportPdfService {
16363
16466
  }
16364
16467
  // Lays every fetched row out once on a scratch document of the same page
16365
16468
  // size, with the options the real draw uses, and reads back how many body
16366
- // rows the box's page holds (fit) and the column widths autoTable chose,
16367
- // with each numeric column's right alignment merged in, so the on-page
16368
- // table and its continuation pages carry both. The totals row is laid out
16369
- // too, so the widths hold it, and a table whose rows fit the box but whose
16370
- // totals row does not moves its last row on. fit 0 means not even one
16371
- // body row fits under the head (or the head itself did not fit): autoTable
16372
- // moved to a new page before drawing. The real document is untouched.
16469
+ // rows the box holds (fit), the column widths autoTable chose, with each
16470
+ // numeric column's right alignment merged in, so the on-page table and its
16471
+ // continuation pages carry both, and the least room a page must give the
16472
+ // table (minHeight). The totals row is laid out too, so the widths hold it,
16473
+ // and a table whose rows fit the box but whose totals row does not moves
16474
+ // its last row on. fit 0 means not even one body row fits under the head.
16475
+ // The table is laid out, never drawn: drawn, autoTable pages it, and a table
16476
+ // with no room for a row on a page pages without end.
16373
16477
  _measure(context, head, cells, numeric, foot, x, y, w, bottom) {
16374
16478
  const scratch = this._scratchDocument(context);
16375
- let fit = 0;
16376
- context.autoTable(scratch, {
16377
- ...this._tableOptions(context, head, numeric, cells, x, y, w, bottom, undefined, foot),
16378
- didDrawCell: (hook) => {
16379
- if (hook.section === 'body' && scratch.getCurrentPageInfo().pageNumber === 1) {
16380
- fit = Math.max(fit, hook.row.index + 1);
16381
- }
16382
- },
16383
- });
16384
- const columns = scratch.lastAutoTable.columns;
16479
+ const table = context.createTable(scratch, this._tableOptions(context, head, numeric, cells, x, y, w, bottom, undefined, foot));
16480
+ const columns = table.columns;
16385
16481
  const columnStyles = {};
16386
16482
  columns.forEach((column, index) => {
16387
16483
  columnStyles[index] = numeric[index]
16388
16484
  ? { cellWidth: column.width, halign: 'right' }
16389
16485
  : { cellWidth: column.width };
16390
16486
  });
16391
- // A row taller than a whole page is split rather than moved, so a last
16392
- // row that started on the first page can still have run past it.
16393
- if (fit === cells.length && scratch.getNumberOfPages() > 1) {
16394
- fit -= 1;
16487
+ const headHeight = table.getHeadHeight(table.columns);
16488
+ const footHeight = table.getFootHeight(table.columns);
16489
+ return {
16490
+ fit: this._fittingRows(table, y + headHeight, context.pageH - bottom, footHeight),
16491
+ columnStyles,
16492
+ // One line of a row: the line autoTable must place on a page before it
16493
+ // moves on, at the table's font size and padding.
16494
+ minHeight: headHeight + footHeight + TABLE_FONT_SIZE * scratch.getLineHeightFactor() + 2 * TABLE_CELL_PADDING,
16495
+ };
16496
+ }
16497
+ // How many of a laid-out table's body rows fit whole between top and
16498
+ // bottom, as autoTable decides it: a row fits when its tallest cell (one that
16499
+ // spans rows counts all of them) ends by bottom, the last row's by the top
16500
+ // of the totals row (footHeight) under it, and the next row starts where it
16501
+ // ends.
16502
+ _fittingRows(table, top, bottom, footHeight) {
16503
+ let cursor = top;
16504
+ let fit = 0;
16505
+ for (const [index, row] of table.body.entries()) {
16506
+ const limit = index === table.body.length - 1 ? bottom - footHeight : bottom;
16507
+ if (cursor + row.getMaxCellHeight(table.columns) > limit) {
16508
+ break;
16509
+ }
16510
+ cursor += row.height;
16511
+ fit += 1;
16395
16512
  }
16396
- return { fit, columnStyles };
16513
+ return fit;
16397
16514
  }
16398
16515
  // A blank document of the export's page size, for laying a table out to see
16399
16516
  // what fits before the real one is drawn on.
@@ -16404,20 +16521,42 @@ class ReportPdfService {
16404
16521
  orientation: context.orientation,
16405
16522
  });
16406
16523
  }
16407
- // Draws an on-page table (a list's, or a text block's), then removes any
16408
- // page the draw added. That should never happen: the measured fit, the
16409
- // identical column widths and a single-line notice keep the table inside its
16410
- // box. The check exists so a stray page can never catch the report page's
16411
- // remaining components.
16524
+ // Draws an on-page table (a list's, or a text block's) on its page alone.
16525
+ // That is all it should need: the measured fit, the identical column widths
16526
+ // and a single-line notice keep the table inside its box. Should autoTable
16527
+ // start another page anyway, the table stops there and the page goes, so a
16528
+ // stray page can never catch the report page's remaining components, nor a
16529
+ // table with no room for a row add pages without end. Returns whether the
16530
+ // table was drawn whole.
16412
16531
  _drawTable(context, options) {
16413
16532
  const { doc } = context;
16414
16533
  const page = doc.getCurrentPageInfo().pageNumber;
16415
16534
  const pages = doc.getNumberOfPages();
16416
- context.autoTable(doc, options);
16535
+ let whole = true;
16536
+ try {
16537
+ context.autoTable(doc, {
16538
+ ...options,
16539
+ // Fires as the table starts its own page, and again on each page it
16540
+ // moves to (an existing page, such as a continuation page under a
16541
+ // header, as well as a new one).
16542
+ willDrawPage: () => {
16543
+ if (doc.getCurrentPageInfo().pageNumber !== page) {
16544
+ throw STRAY_PAGE;
16545
+ }
16546
+ },
16547
+ });
16548
+ }
16549
+ catch (error) {
16550
+ if (error !== STRAY_PAGE) {
16551
+ throw error;
16552
+ }
16553
+ whole = false;
16554
+ }
16417
16555
  while (doc.getNumberOfPages() > pages) {
16418
16556
  doc.deletePage(doc.getNumberOfPages());
16419
16557
  }
16420
16558
  doc.setPage(page);
16559
+ return whole;
16421
16560
  }
16422
16561
  // The rows of one list that did not fit its box, on pages appended after
16423
16562
  // the report page, each with the page's header and footer, the table's
@@ -16426,9 +16565,7 @@ class ReportPdfService {
16426
16565
  // Returns the physical page counter after these pages.
16427
16566
  async _renderContinuation(context, page, index, pageNumber, overflow) {
16428
16567
  const { doc } = context;
16429
- const bands = page.masterHidden ? NO_BANDS : context.report.config?.headerFooter ?? NO_BANDS;
16430
- const top = this._continuationMargin(bands.headerHeight);
16431
- const bottom = this._continuationMargin(bands.footerHeight);
16568
+ const { top, bottom } = this._continuationMargins(context, page);
16432
16569
  const { x, w } = overflow;
16433
16570
  const body = overflow.omitted > 0
16434
16571
  ? [...overflow.rows, this._noticeRow(moreRowsNotice(overflow.omitted), overflow.numeric.length, false)]
@@ -16462,6 +16599,21 @@ class ReportPdfService {
16462
16599
  }
16463
16600
  doc.setPage(doc.getNumberOfPages());
16464
16601
  }
16602
+ // The margins a continuation table keeps from the top and bottom of its
16603
+ // pages, under its title's strip at the top.
16604
+ _continuationMargins(context, page) {
16605
+ const bands = page.masterHidden ? NO_BANDS : context.report.config?.headerFooter ?? NO_BANDS;
16606
+ return {
16607
+ top: this._continuationMargin(bands.headerHeight),
16608
+ bottom: this._continuationMargin(bands.footerHeight),
16609
+ };
16610
+ }
16611
+ // The height a continuation page gives its table: between its margins, under
16612
+ // the title.
16613
+ _continuationRoom(context, page) {
16614
+ const { top, bottom } = this._continuationMargins(context, page);
16615
+ return context.pageH - top - TITLE_H$1 - bottom;
16616
+ }
16465
16617
  // The margin a continuation table keeps from one edge: inside the band when
16466
16618
  // the page has one.
16467
16619
  _continuationMargin(band) {
@@ -16827,6 +16979,9 @@ class ReportPptxService {
16827
16979
  return {
16828
16980
  overflows: [...overflows.values()],
16829
16981
  overflowNote: PPTX_OVERFLOW_NOTE,
16982
+ // A slide's shapes are laid out by PowerPoint, so a component cannot
16983
+ // fail to draw here as a PDF table can.
16984
+ undrawn: [],
16830
16985
  save: async () => {
16831
16986
  await pptx.writeFile({ fileName: `${this._slug(report.name)}.pptx` });
16832
16987
  },
@@ -18005,7 +18160,8 @@ const RETRY = 'retry';
18005
18160
  const DOWNLOAD = 'download';
18006
18161
  // Runs a report export end to end: collect every tile and value, refuse to
18007
18162
  // write a file that is missing any of them without saying so, build it, refuse
18008
- // to save one whose text does not fit its boxes without saying so, then save.
18163
+ // to save one that could not draw a component, or whose text does not fit its
18164
+ // boxes, without saying so, then save.
18009
18165
  //
18010
18166
  // The gates are the reason this sits between the collector and the renderers
18011
18167
  // rather than in either of them. A deck with five blank pages is worse than no
@@ -18044,11 +18200,12 @@ class ReportExportService {
18044
18200
  ? this._run(report, listRows, build)
18045
18201
  : this._buildAndSave(collected, build)))));
18046
18202
  }
18047
- // Builds the file, then saves it once the overflow gate lets it through.
18203
+ // Builds the file, then saves it once the undrawn and overflow gates let it
18204
+ // through.
18048
18205
  _buildAndSave(collected, build) {
18049
18206
  return from(build(collected))
18050
- .pipe(switchMap((built) => this._overflowGate(built)
18051
- .pipe(switchMap(() => from(Promise.resolve(built.save()))))));
18207
+ .pipe(switchMap((built) => this._undrawnGate(built)
18208
+ .pipe(switchMap(() => this._overflowGate(built)), switchMap(() => from(Promise.resolve(built.save()))))));
18052
18209
  }
18053
18210
  // Resolves to DOWNLOAD when the export is whole (or the user accepted an
18054
18211
  // incomplete one) and to RETRY when they want another attempt. Cancelling
@@ -18074,6 +18231,30 @@ class ReportExportService {
18074
18231
  })
18075
18232
  .pipe(catchError(() => EMPTY));
18076
18233
  }
18234
+ // Resolves to DOWNLOAD when the build drew every component, or the user
18235
+ // accepted a file that marks the ones it could not in their place. There is
18236
+ // no Retry: a second build draws the same data the same way. Each component
18237
+ // is named by its page and its title (a text block by the start of its
18238
+ // words). Cancelling ends the export quietly, as at the failure gate, and
18239
+ // the built file is dropped unsaved.
18240
+ _undrawnGate(built) {
18241
+ if (!built.undrawn.length) {
18242
+ return of(DOWNLOAD);
18243
+ }
18244
+ const components = built.undrawn
18245
+ .map((undrawn) => `<strong>${escapeHtml(undrawn.page)}</strong>`
18246
+ + (undrawn.name ? ` (“${escapeHtml(undrawn.name)}”)` : ''));
18247
+ return this._prompt.confirm({
18248
+ title: 'Some tiles could not be drawn',
18249
+ template: `Some tiles could not be drawn: ${components.join(', ')}. `
18250
+ + 'The file says "Could not be drawn" in their place.',
18251
+ buttons: [
18252
+ { label: 'Download Anyway', value: DOWNLOAD, color: 'primary' },
18253
+ { label: 'Cancel', cancel: true, color: '' },
18254
+ ],
18255
+ })
18256
+ .pipe(catchError(() => EMPTY));
18257
+ }
18077
18258
  // Resolves to DOWNLOAD when every text block fits its box in the built file,
18078
18259
  // or the user accepted one that does not. There is no Retry: the words are
18079
18260
  // the report's own, so a second attempt would build the same file. Each block
@@ -18878,7 +19059,7 @@ class FsAiReportComponent {
18878
19059
  ReportAiChatData,
18879
19060
  { provide: FsAiChatData, useExisting: ReportAiChatData },
18880
19061
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
18881
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-7AUAX6RO.mjs').then((module) => module.default) }),
19062
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CBoGhanh.mjs').then((module) => module.default) }),
18882
19063
  // The bar's date pickers start their weeks on Monday and list the built-in
18883
19064
  // presets; a preset only sets the days.
18884
19065
  provideReportDatePicker(),
@@ -18902,7 +19083,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
18902
19083
  ReportAiChatData,
18903
19084
  { provide: FsAiChatData, useExisting: ReportAiChatData },
18904
19085
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
18905
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-7AUAX6RO.mjs').then((module) => module.default) }),
19086
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CBoGhanh.mjs').then((module) => module.default) }),
18906
19087
  // The bar's date pickers start their weeks on Monday and list the built-in
18907
19088
  // presets; a preset only sets the days.
18908
19089
  provideReportDatePicker(),
@@ -18995,4 +19176,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
18995
19176
  */
18996
19177
 
18997
19178
  export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, REPORT_CHART_FONT_FAMILY as a, REPORT_AXIS_LABEL_FONT_SIZE as b, FsAiReportComponent as c, ReportData as d, ReportService as e, ReportFilterStateService as f, ReportNodeType as g, ReportState as h, ReportValueFormat as i, ReportValueFormula as j, ReportListColumnFormat as k, ReportNumberUnits as l, ReportChartDataLabels as m, ReportChartColor as n, ReportChartLineStyle as o, ReportChartMarkStat as p, ReportChartDateLabel as q, ReportChangePolarity as r };
18998
- //# sourceMappingURL=firestitch-report-firestitch-report-DgeCapUH.mjs.map
19179
+ //# sourceMappingURL=firestitch-report-firestitch-report-MBuBuGiA.mjs.map