angular-my-report 0.1.0 → 0.1.1

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.
Files changed (88) hide show
  1. package/README.md +124 -40
  2. package/fesm2022/angular-my-report.mjs +7931 -0
  3. package/fesm2022/angular-my-report.mjs.map +1 -0
  4. package/index.d.ts +5 -0
  5. package/lib/engine/barcode.d.ts +17 -0
  6. package/lib/engine/chart.d.ts +26 -0
  7. package/lib/engine/chartLayout.d.ts +45 -0
  8. package/lib/engine/crosstab.d.ts +22 -0
  9. package/lib/engine/dataSources.d.ts +35 -0
  10. package/lib/engine/expressions.d.ts +46 -0
  11. package/lib/engine/measure.d.ts +7 -0
  12. package/lib/engine/parameters.d.ts +15 -0
  13. package/lib/engine/qr.d.ts +8 -0
  14. package/lib/engine/renderer.d.ts +43 -0
  15. package/lib/engine/table.d.ts +22 -0
  16. package/lib/model/defaults.d.ts +31 -0
  17. package/lib/model/factories.d.ts +31 -0
  18. package/lib/model/types.d.ts +244 -0
  19. package/lib/ui/css.d.ts +2 -0
  20. package/lib/ui/designer/banded-table-dialog.component.d.ts +27 -0
  21. package/lib/ui/designer/canvas.component.d.ts +41 -0
  22. package/lib/ui/designer/data-source-dialog.component.d.ts +37 -0
  23. package/lib/ui/designer/element-box.component.d.ts +32 -0
  24. package/lib/ui/designer/graphics.components.d.ts +36 -0
  25. package/lib/ui/designer/left-panel.component.d.ts +37 -0
  26. package/lib/ui/designer/safe-image.pipe.d.ts +13 -0
  27. package/lib/ui/designer/table-view.component.d.ts +19 -0
  28. package/lib/ui/designer.component.d.ts +59 -0
  29. package/lib/ui/designer.store.d.ts +82 -0
  30. package/lib/ui/inputs.d.ts +8 -0
  31. package/lib/ui/properties/band-props.component.d.ts +15 -0
  32. package/lib/ui/properties/element-props.component.d.ts +46 -0
  33. package/lib/ui/properties/expr-input.component.d.ts +43 -0
  34. package/lib/ui/properties/graphics-props.component.d.ts +52 -0
  35. package/lib/ui/properties/image-picker.component.d.ts +15 -0
  36. package/lib/ui/properties/properties-panel.component.d.ts +13 -0
  37. package/lib/ui/properties/report-extras.component.d.ts +40 -0
  38. package/lib/ui/properties/report-props.component.d.ts +34 -0
  39. package/lib/ui/properties/table-props.component.d.ts +31 -0
  40. package/lib/ui/viewer/page-view.component.d.ts +17 -0
  41. package/lib/ui/viewer/render.d.ts +43 -0
  42. package/lib/ui/viewer/viewer.component.d.ts +99 -0
  43. package/lib/ui/viewer.component.d.ts +40 -0
  44. package/package.json +16 -5
  45. package/{src/public-api.ts → public-api.d.ts} +0 -5
  46. package/ng-package.json +0 -7
  47. package/src/lib/engine/barcode.ts +0 -118
  48. package/src/lib/engine/chart.ts +0 -85
  49. package/src/lib/engine/chartLayout.ts +0 -265
  50. package/src/lib/engine/crosstab.ts +0 -96
  51. package/src/lib/engine/dataSources.ts +0 -204
  52. package/src/lib/engine/expressions.ts +0 -734
  53. package/src/lib/engine/measure.ts +0 -62
  54. package/src/lib/engine/parameters.ts +0 -96
  55. package/src/lib/engine/qr.ts +0 -26
  56. package/src/lib/engine/renderer.ts +0 -324
  57. package/src/lib/engine/table.ts +0 -58
  58. package/src/lib/model/defaults.ts +0 -249
  59. package/src/lib/model/factories.ts +0 -324
  60. package/src/lib/model/types.ts +0 -281
  61. package/src/lib/ui/css.ts +0 -12
  62. package/src/lib/ui/designer/banded-table-dialog.component.ts +0 -127
  63. package/src/lib/ui/designer/canvas.component.ts +0 -281
  64. package/src/lib/ui/designer/data-source-dialog.component.ts +0 -201
  65. package/src/lib/ui/designer/element-box.component.ts +0 -138
  66. package/src/lib/ui/designer/graphics.components.ts +0 -140
  67. package/src/lib/ui/designer/left-panel.component.ts +0 -189
  68. package/src/lib/ui/designer/safe-image.pipe.ts +0 -16
  69. package/src/lib/ui/designer/table-view.component.ts +0 -120
  70. package/src/lib/ui/designer.component.ts +0 -296
  71. package/src/lib/ui/designer.store.ts +0 -230
  72. package/src/lib/ui/inputs.ts +0 -34
  73. package/src/lib/ui/myreport.css +0 -267
  74. package/src/lib/ui/properties/band-props.component.ts +0 -86
  75. package/src/lib/ui/properties/element-props.component.ts +0 -326
  76. package/src/lib/ui/properties/expr-input.component.ts +0 -191
  77. package/src/lib/ui/properties/graphics-props.component.ts +0 -260
  78. package/src/lib/ui/properties/image-picker.component.ts +0 -62
  79. package/src/lib/ui/properties/properties-panel.component.ts +0 -37
  80. package/src/lib/ui/properties/report-extras.component.ts +0 -198
  81. package/src/lib/ui/properties/report-props.component.ts +0 -211
  82. package/src/lib/ui/properties/table-props.component.ts +0 -197
  83. package/src/lib/ui/viewer/page-view.component.ts +0 -73
  84. package/src/lib/ui/viewer/render.ts +0 -161
  85. package/src/lib/ui/viewer/viewer.component.ts +0 -555
  86. package/src/lib/ui/viewer.component.ts +0 -107
  87. package/tsconfig.lib.json +0 -12
  88. package/tsconfig.lib.prod.json +0 -5
@@ -1,260 +0,0 @@
1
- import { ChangeDetectionStrategy, Component, Directive, computed, inject, input } from '@angular/core';
2
- import { FormsModule } from '@angular/forms';
3
- import type { AggregateKind, ChartType, FormatKind, ReportElement } from '../../model/types';
4
- import { MAX_SERIES } from '../../engine/chart';
5
- import { DesignerStore, findElement } from '../designer.store';
6
-
7
- const AGGREGATES: AggregateKind[] = ['sum', 'count', 'avg', 'min', 'max'];
8
- const VALUE_FORMATS: FormatKind[] = ['none', 'number', 'currency', 'percent'];
9
-
10
- /** Base: edits one element in place by id (one undo step per change). */
11
- @Directive()
12
- abstract class SpecEditor {
13
- protected readonly store = inject(DesignerStore);
14
- readonly el = input.required<ReportElement>();
15
- protected readonly aggregates = AGGREGATES;
16
- protected readonly valueFormats = VALUE_FORMATS;
17
-
18
- protected edit(fn: (e: ReportElement) => void): void {
19
- const id = this.el().id;
20
- this.store.update((r) => {
21
- const f = findElement(r, id);
22
- if (f) fn(f.el);
23
- });
24
- }
25
-
26
- protected positive(v: number | null, fn: (n: number) => void): void {
27
- if (v != null && Number.isFinite(v)) fn(Math.max(0, v));
28
- }
29
- }
30
-
31
- @Component({
32
- selector: 'rpt-chart-props',
33
- standalone: true,
34
- imports: [FormsModule],
35
- changeDetection: ChangeDetectionStrategy.OnPush,
36
- template: `
37
- @let c = el().chart!;
38
- <section>
39
- <h4>Chart</h4>
40
- <div class="btn-group">
41
- @for (t of types; track t.value) {
42
- <button [class.on]="c.type === t.value" [title]="t.label" (click)="edit(set('type', t.value))">{{ t.icon }} {{ t.label }}</button>
43
- }
44
- </div>
45
- <label class="prop-row"><span>Title</span>
46
- <div class="prop-value"><input [ngModel]="c.title" placeholder="e.g. Sales in {Country}" (ngModelChange)="edit(set('title', $event))" /></div>
47
- </label>
48
- <label class="prop-row"><span>Category</span>
49
- <div class="prop-value"><input list="field-names" [ngModel]="c.categoryField" placeholder="Field" (ngModelChange)="edit(set('categoryField', $event))" /></div>
50
- </label>
51
- <label class="prop-row"><span>{{ pie() ? 'Value' : 'Values' }}</span>
52
- <div class="prop-value">
53
- <input list="field-names" [ngModel]="c.valueFields.join(', ')" [placeholder]="pie() ? 'Field' : 'Field, Field…'" (ngModelChange)="setValues($event)" />
54
- </div>
55
- </label>
56
- <label class="prop-row"><span>Aggregate</span>
57
- <div class="prop-value">
58
- <select [ngModel]="c.aggregate" (ngModelChange)="edit(set('aggregate', $event))">
59
- @for (a of aggregates; track a) { <option [value]="a">{{ a }}</option> }
60
- </select>
61
- </div>
62
- </label>
63
- <label class="prop-row"><span>Sort</span>
64
- <div class="prop-value">
65
- <select [ngModel]="c.sort" (ngModelChange)="edit(set('sort', $event))">
66
- <option value="none">Data order</option>
67
- <option value="category">By category</option>
68
- <option value="valueDesc">Value, high → low</option>
69
- <option value="valueAsc">Value, low → high</option>
70
- </select>
71
- </div>
72
- </label>
73
- <label class="prop-row"><span>Max items</span>
74
- <div class="prop-value"><input type="number" min="1" [ngModel]="c.maxCategories" (ngModelChange)="positive($event, setMax)" /></div>
75
- </label>
76
- <label class="prop-row"><span>Value format</span>
77
- <div class="prop-value">
78
- <select [ngModel]="c.valueFormat.kind" (ngModelChange)="edit(setFormat($event))">
79
- @for (k of valueFormats; track k) { <option [value]="k">{{ k }}</option> }
80
- </select>
81
- </div>
82
- </label>
83
- <label class="check"><input type="checkbox" [ngModel]="c.showLegend" (ngModelChange)="edit(set('showLegend', $event))" /> Legend</label>
84
- <label class="check"><input type="checkbox" [ngModel]="c.showValues" (ngModelChange)="edit(set('showValues', $event))" /> Value labels</label>
85
- @if (!pie()) {
86
- <label class="check"><input type="checkbox" [ngModel]="c.showGrid" (ngModelChange)="edit(set('showGrid', $event))" /> Grid lines</label>
87
- }
88
- <p class="hint">
89
- Charts the rows of its band: all rows in Report Header/Footer, the group's rows in a Group band.
90
- @if (pie()) { Pie slices beyond {{ maxSeries }} fold into "Other". } @else { Up to {{ maxSeries }} value fields (one colour each). }
91
- </p>
92
- </section>
93
- `,
94
- })
95
- export class ChartPropsComponent extends SpecEditor {
96
- protected readonly maxSeries = MAX_SERIES;
97
- protected readonly types: { value: ChartType; label: string; icon: string }[] = [
98
- { value: 'bar', label: 'Bar', icon: '▮' },
99
- { value: 'hbar', label: 'H-bar', icon: '▬' },
100
- { value: 'line', label: 'Line', icon: '⟋' },
101
- { value: 'area', label: 'Area', icon: '◭' },
102
- { value: 'pie', label: 'Pie', icon: '◔' },
103
- { value: 'doughnut', label: 'Doughnut', icon: '◯' },
104
- ];
105
- protected readonly pie = computed(() => this.el().chart?.type === 'pie' || this.el().chart?.type === 'doughnut');
106
- protected set = <K extends keyof NonNullable<ReportElement['chart']>>(k: K, v: NonNullable<ReportElement['chart']>[K]) => (e: ReportElement) =>
107
- (e.chart![k] = v);
108
- protected setMax = (n: number) => this.edit((e) => (e.chart!.maxCategories = Math.max(1, Math.round(n))));
109
- protected setFormat = (k: FormatKind) => (e: ReportElement) => (e.chart!.valueFormat.kind = k);
110
- protected setValues(v: string): void {
111
- const fields = v.split(',').map((s) => s.trim()).filter(Boolean).slice(0, MAX_SERIES);
112
- this.edit((e) => (e.chart!.valueFields = fields));
113
- }
114
- }
115
-
116
- @Component({
117
- selector: 'rpt-barcode-props',
118
- standalone: true,
119
- imports: [FormsModule],
120
- changeDetection: ChangeDetectionStrategy.OnPush,
121
- template: `
122
- @let b = el().barcode!;
123
- <section>
124
- <h4>Barcode</h4>
125
- <label class="prop-row"><span>Type</span>
126
- <div class="prop-value">
127
- <select [ngModel]="b.symbology" (ngModelChange)="edit(setType($event))">
128
- <option value="code128">Code 128 (any text)</option>
129
- <option value="ean13">EAN-13 (12–13 digits)</option>
130
- </select>
131
- </div>
132
- </label>
133
- <label class="check"><input type="checkbox" [ngModel]="b.showText" (ngModelChange)="edit(setShowText($event))" /> Show text under bars</label>
134
- <label class="prop-row"><span>Bar color</span>
135
- <div class="prop-value"><input type="color" [ngModel]="b.color" (ngModelChange)="edit(setColor($event))" /></div>
136
- </label>
137
- <p class="hint">Keep bars dark on a light background for reliable scanning. EAN-13 adds the check digit when you give 12 digits.</p>
138
- </section>
139
- `,
140
- })
141
- export class BarcodePropsComponent extends SpecEditor {
142
- protected setType = (v: 'code128' | 'ean13') => (e: ReportElement) => (e.barcode!.symbology = v);
143
- protected setShowText = (v: boolean) => (e: ReportElement) => (e.barcode!.showText = v);
144
- protected setColor = (v: string) => (e: ReportElement) => (e.barcode!.color = v);
145
- }
146
-
147
- @Component({
148
- selector: 'rpt-qr-props',
149
- standalone: true,
150
- imports: [FormsModule],
151
- changeDetection: ChangeDetectionStrategy.OnPush,
152
- template: `
153
- @let q = el().qr!;
154
- <section>
155
- <h4>QR code</h4>
156
- <label class="prop-row"><span>Error level</span>
157
- <div class="prop-value">
158
- <select [ngModel]="q.errorCorrection" (ngModelChange)="edit(setLevel($event))">
159
- <option value="L">L — 7% (smallest)</option>
160
- <option value="M">M — 15%</option>
161
- <option value="Q">Q — 25%</option>
162
- <option value="H">H — 30% (most robust)</option>
163
- </select>
164
- </div>
165
- </label>
166
- <label class="prop-row"><span>Color</span>
167
- <div class="prop-value"><input type="color" [ngModel]="q.color" (ngModelChange)="edit(setColor($event))" /></div>
168
- </label>
169
- </section>
170
- `,
171
- })
172
- export class QrPropsComponent extends SpecEditor {
173
- protected setLevel = (v: 'L' | 'M' | 'Q' | 'H') => (e: ReportElement) => (e.qr!.errorCorrection = v);
174
- protected setColor = (v: string) => (e: ReportElement) => (e.qr!.color = v);
175
- }
176
-
177
- @Component({
178
- selector: 'rpt-crosstab-props',
179
- standalone: true,
180
- imports: [FormsModule],
181
- changeDetection: ChangeDetectionStrategy.OnPush,
182
- template: `
183
- @let x = el().crosstab!;
184
- <section>
185
- <h4>Cross table</h4>
186
- <label class="prop-row"><span>Rows</span>
187
- <div class="prop-value"><input list="field-names" [ngModel]="x.rowField" placeholder="Field" (ngModelChange)="edit(set('rowField', $event))" /></div>
188
- </label>
189
- <label class="prop-row"><span>Columns</span>
190
- <div class="prop-value"><input list="field-names" [ngModel]="x.columnField" placeholder="Field" (ngModelChange)="edit(set('columnField', $event))" /></div>
191
- </label>
192
- <label class="prop-row"><span>Value</span>
193
- <div class="prop-value"><input list="field-names" [ngModel]="x.valueField" placeholder="Field (empty = count)" (ngModelChange)="edit(set('valueField', $event))" /></div>
194
- </label>
195
- <label class="prop-row"><span>Aggregate</span>
196
- <div class="prop-value">
197
- <select [ngModel]="x.aggregate" (ngModelChange)="edit(set('aggregate', $event))">
198
- @for (a of aggregates; track a) { <option [value]="a">{{ a }}</option> }
199
- </select>
200
- </div>
201
- </label>
202
- <div class="grid2">
203
- <label class="prop-row"><span>Format</span>
204
- <div class="prop-value">
205
- <select [ngModel]="x.valueFormat.kind" (ngModelChange)="edit(setFormat($event))">
206
- @for (k of valueFormats; track k) { <option [value]="k">{{ k }}</option> }
207
- </select>
208
- </div>
209
- </label>
210
- <label class="prop-row"><span>Decimals</span>
211
- <div class="prop-value"><input type="number" min="0" [ngModel]="x.valueFormat.decimals" (ngModelChange)="positive($event, setDecimals)" /></div>
212
- </label>
213
- </div>
214
- <label class="prop-row"><span>Corner text</span>
215
- <div class="prop-value"><input [ngModel]="x.cornerText" placeholder="Rows / Columns" (ngModelChange)="edit(set('cornerText', $event))" /></div>
216
- </label>
217
- <label class="check"><input type="checkbox" [ngModel]="x.showRowTotals" (ngModelChange)="edit(set('showRowTotals', $event))" /> Row totals (right column)</label>
218
- <label class="check"><input type="checkbox" [ngModel]="x.showColumnTotals" (ngModelChange)="edit(set('showColumnTotals', $event))" /> Column totals (bottom row)</label>
219
- <label class="check"><input type="checkbox" [ngModel]="x.repeatHeader" (ngModelChange)="edit(set('repeatHeader', $event))" /> Repeat header on each page</label>
220
- </section>
221
- <section>
222
- <h4>Look</h4>
223
- <div class="grid2">
224
- <label class="prop-row"><span>Header h</span>
225
- <div class="prop-value"><input type="number" min="8" [ngModel]="x.headerHeight" (ngModelChange)="positive($event, setHeaderH)" /></div>
226
- </label>
227
- <label class="prop-row"><span>Row h</span>
228
- <div class="prop-value"><input type="number" min="8" [ngModel]="x.rowHeight" (ngModelChange)="positive($event, setRowH)" /></div>
229
- </label>
230
- </div>
231
- <div class="prop-row"><span>Header fill</span>
232
- <div class="prop-value"><input type="color" [ngModel]="x.headerBackground" (ngModelChange)="edit(set('headerBackground', $event))" /></div>
233
- </div>
234
- <div class="prop-row"><span>Header text</span>
235
- <div class="prop-value"><input type="color" [ngModel]="x.headerColor" (ngModelChange)="edit(set('headerColor', $event))" /></div>
236
- </div>
237
- <div class="prop-row"><span>Alt. row</span>
238
- <div class="prop-value">
239
- <input type="color" [ngModel]="x.stripeBackground === 'transparent' ? '#ffffff' : x.stripeBackground" (ngModelChange)="edit(set('stripeBackground', $event))" />
240
- <button class="small" [class.on]="x.stripeBackground === 'transparent'" (click)="edit(set('stripeBackground', 'transparent'))">None</button>
241
- </div>
242
- </div>
243
- <div class="prop-row"><span>Grid lines</span>
244
- <div class="prop-value">
245
- <input type="color" [ngModel]="x.gridColor === 'transparent' ? '#ffffff' : x.gridColor" (ngModelChange)="edit(set('gridColor', $event))" />
246
- <button class="small" [class.on]="x.gridColor === 'transparent'" (click)="edit(set('gridColor', 'transparent'))">None</button>
247
- </div>
248
- </div>
249
- <p class="hint">Pivots the rows of its band. It grows to fit and continues onto new pages in the preview.</p>
250
- </section>
251
- `,
252
- })
253
- export class CrosstabPropsComponent extends SpecEditor {
254
- protected set = <K extends keyof NonNullable<ReportElement['crosstab']>>(k: K, v: NonNullable<ReportElement['crosstab']>[K]) => (e: ReportElement) =>
255
- (e.crosstab![k] = v);
256
- protected setFormat = (k: FormatKind) => (e: ReportElement) => (e.crosstab!.valueFormat.kind = k);
257
- protected setDecimals = (n: number) => this.edit((e) => (e.crosstab!.valueFormat.decimals = Math.min(10, Math.round(n))));
258
- protected setHeaderH = (n: number) => this.edit((e) => (e.crosstab!.headerHeight = Math.max(8, n)));
259
- protected setRowH = (n: number) => this.edit((e) => (e.crosstab!.rowHeight = Math.max(8, n)));
260
- }
@@ -1,62 +0,0 @@
1
- import { ChangeDetectionStrategy, Component, computed, input, output, signal } from '@angular/core';
2
- import { SafeImagePipe } from '../designer/safe-image.pipe';
3
-
4
- const MAX_BYTES = 5 * 1024 * 1024;
5
-
6
- /** Image source: a URL (may contain {Field}) or an uploaded file embedded as a base64 data URL. */
7
- @Component({
8
- selector: 'rpt-image-picker',
9
- standalone: true,
10
- imports: [SafeImagePipe],
11
- changeDetection: ChangeDetectionStrategy.OnPush,
12
- template: `
13
- <div class="image-picker">
14
- @if (embedded()) {
15
- <div class="muted">Embedded image · {{ size() }}</div>
16
- } @else {
17
- <input [value]="value()" [placeholder]="placeholder()" (input)="valueChange.emit($any($event.target).value)" />
18
- }
19
- <div class="inline">
20
- <button class="small" (click)="file.click()">Upload…</button>
21
- @if (value()) {
22
- <button class="small" (click)="valueChange.emit('')">Remove</button>
23
- }
24
- <input #file type="file" accept="image/*" hidden (change)="upload($event)" />
25
- </div>
26
- @if (value() && !value().includes('{')) {
27
- <img class="thumb" alt="" [src]="value() | safeImage" />
28
- }
29
- @if (message()) {
30
- <div class="error">{{ message() }}</div>
31
- }
32
- </div>
33
- `,
34
- })
35
- export class ImagePickerComponent {
36
- readonly value = input('');
37
- readonly placeholder = input('https://… or {Field}');
38
- readonly valueChange = output<string>();
39
-
40
- protected readonly message = signal('');
41
- protected readonly embedded = computed(() => this.value().startsWith('data:'));
42
- /** Approximate decoded size of the embedded image. */
43
- protected readonly size = computed(() => {
44
- const bytes = Math.round(((this.value().length - this.value().indexOf(',') - 1) * 3) / 4);
45
- return bytes < 1024 ? `${bytes} bytes` : `${(bytes / 1024).toFixed(bytes < 10240 ? 1 : 0)} KB`;
46
- });
47
-
48
- /** Reads the file as base64 (data URL) so the image travels inside the report file. */
49
- protected upload(e: Event): void {
50
- const inputEl = e.target as HTMLInputElement;
51
- const f = inputEl.files?.[0];
52
- inputEl.value = '';
53
- if (!f) return;
54
- if (!f.type.startsWith('image/')) return this.message.set('Please choose an image file.');
55
- if (f.size > MAX_BYTES) return this.message.set(`Image is ${(f.size / 1048576).toFixed(1)} MB; the limit is 5 MB.`);
56
- this.message.set('');
57
- const reader = new FileReader();
58
- reader.onload = () => this.valueChange.emit(String(reader.result));
59
- reader.onerror = () => this.message.set('Could not read the file.');
60
- reader.readAsDataURL(f);
61
- }
62
- }
@@ -1,37 +0,0 @@
1
- import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2
- import { DesignerStore, findElement } from '../designer.store';
3
- import { ElementPropsComponent } from './element-props.component';
4
- import { BandPropsComponent } from './band-props.component';
5
- import { ReportPropsComponent } from './report-props.component';
6
-
7
- /** Right-hand panel: properties of the selected element(s), else the selected band, else the report. */
8
- @Component({
9
- selector: 'rpt-properties-panel',
10
- standalone: true,
11
- imports: [ElementPropsComponent, BandPropsComponent, ReportPropsComponent],
12
- changeDetection: ChangeDetectionStrategy.OnPush,
13
- template: `
14
- <aside class="panel right-panel">
15
- <datalist id="field-names">
16
- @for (f of store.data().fields; track f.name) {
17
- <option [value]="f.name"></option>
18
- }
19
- </datalist>
20
- @if (first(); as f) {
21
- <rpt-element-props [el]="f.el" [count]="store.selection().elementIds.length" />
22
- } @else if (bandId()) {
23
- <rpt-band-props [bandId]="bandId()!" />
24
- } @else {
25
- <rpt-report-props />
26
- }
27
- </aside>
28
- `,
29
- })
30
- export class PropertiesPanelComponent {
31
- protected readonly store = inject(DesignerStore);
32
- protected readonly first = computed(() => {
33
- const ids = this.store.selection().elementIds;
34
- return ids.length ? findElement(this.store.report(), ids[0]) : null;
35
- });
36
- protected readonly bandId = computed(() => this.store.selection().bandId);
37
- }
@@ -1,198 +0,0 @@
1
- import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2
- import { FormsModule } from '@angular/forms';
3
- import type { PageBackground, ParameterType, ReportParameter, Watermark } from '../../model/types';
4
- import { defaultBackground, defaultWatermark } from '../../model/defaults';
5
- import { DesignerStore } from '../designer.store';
6
- import { ExprInputComponent } from './expr-input.component';
7
- import { ImagePickerComponent } from './image-picker.component';
8
-
9
- /** Report-level settings: parameters, row filter, watermark and page background. */
10
- @Component({
11
- selector: 'rpt-report-extras',
12
- standalone: true,
13
- imports: [FormsModule, ExprInputComponent, ImagePickerComponent],
14
- changeDetection: ChangeDetectionStrategy.OnPush,
15
- template: `
16
- <section>
17
- <h4>Parameters</h4>
18
- @for (p of parameters(); track $index; let i = $index) {
19
- <div class="col-card">
20
- <div class="col-head">
21
- <strong>&#64;{{ p.name || '?' }}</strong><span class="spacer"></span>
22
- <button class="small danger" title="Remove parameter" (click)="removeParam(i)">✕</button>
23
- </div>
24
- <div class="grid2">
25
- <label class="prop-row"><span>Name</span>
26
- <div class="prop-value"><input [ngModel]="p.name" placeholder="Country" (ngModelChange)="setParam(i, 'name', clean($event))" /></div>
27
- </label>
28
- <label class="prop-row"><span>Type</span>
29
- <div class="prop-value">
30
- <select [ngModel]="p.type" (ngModelChange)="setParam(i, 'type', $event)">
31
- @for (t of types; track t) { <option [value]="t">{{ t }}</option> }
32
- </select>
33
- </div>
34
- </label>
35
- </div>
36
- <label class="prop-row"><span>Label</span>
37
- <div class="prop-value"><input [ngModel]="p.label" placeholder="Shown in the preview" (ngModelChange)="setParam(i, 'label', $event)" /></div>
38
- </label>
39
- <label class="prop-row"><span>Default</span>
40
- <div class="prop-value"><input [ngModel]="p.defaultValue" (ngModelChange)="setParam(i, 'defaultValue', $event)" /></div>
41
- </label>
42
- <label class="prop-row"><span>Choices</span>
43
- <div class="prop-value"><input [ngModel]="p.options" placeholder="Egypt, USA, France (optional)" (ngModelChange)="setParam(i, 'options', $event)" /></div>
44
- </label>
45
- </div>
46
- }
47
- <button (click)="addParam()">+ Add parameter</button>
48
- <p class="hint">
49
- Use as <code>&#64;Name</code> in formulas, <code>{{ '{' }}&#64;Name{{ '}' }}</code> in the data source URL or OData filter
50
- (e.g. <code>Country eq '{{ '{' }}&#64;Country{{ '}' }}'</code>), or pass in the page address: <code>?Country=Egypt</code>.
51
- </p>
52
- </section>
53
-
54
- <section>
55
- <h4>Filter rows</h4>
56
- <rpt-expr-input mode="expression" [value]="filter()" placeholder="e.g. Country == @Country" (valueChange)="setFilter($event)" />
57
- <p class="hint">Only rows where this is true are printed. Empty = all rows.</p>
58
- </section>
59
-
60
- <section>
61
- @let w = watermark();
62
- <h4>Watermark</h4>
63
- <label class="check"><input type="checkbox" [ngModel]="w.enabled" (ngModelChange)="setWm('enabled', $event)" /> Show watermark</label>
64
- @if (w.enabled) {
65
- <div class="tabs">
66
- <button [class.on]="w.kind === 'text'" (click)="setWm('kind', 'text')">Text</button>
67
- <button [class.on]="w.kind === 'image'" (click)="setWm('kind', 'image')">Image</button>
68
- </div>
69
- @if (w.kind === 'text') {
70
- <rpt-expr-input [value]="w.text" placeholder="CONFIDENTIAL" (valueChange)="setWm('text', $event)" />
71
- <div class="grid2">
72
- <label class="prop-row"><span>Size</span>
73
- <div class="prop-value"><input type="number" min="8" [ngModel]="w.fontSize" (ngModelChange)="num($event, 'fontSize', 8)" /></div>
74
- </label>
75
- <div class="prop-row"><span>Color</span>
76
- <div class="prop-value">
77
- <input type="color" [ngModel]="w.color" (ngModelChange)="setWm('color', $event)" />
78
- <label class="check"><input type="checkbox" [ngModel]="w.bold" (ngModelChange)="setWm('bold', $event)" /> Bold</label>
79
- </div>
80
- </div>
81
- </div>
82
- } @else {
83
- <rpt-image-picker [value]="w.image" placeholder="https://…/logo.png" (valueChange)="setWm('image', $event)" />
84
- <label class="prop-row"><span>Width %</span>
85
- <div class="prop-value"><input type="number" min="5" max="200" [ngModel]="w.imageScale" (ngModelChange)="num($event, 'imageScale', 5)" /></div>
86
- </label>
87
- }
88
- <label class="prop-row"><span>Opacity</span>
89
- <div class="prop-value">
90
- <input type="range" min="0.02" max="1" step="0.01" [ngModel]="w.opacity" (ngModelChange)="setWm('opacity', +$event)" />
91
- <span class="muted">{{ (w.opacity * 100).toFixed(0) }}%</span>
92
- </div>
93
- </label>
94
- <label class="prop-row"><span>Angle</span>
95
- <div class="prop-value">
96
- <input type="range" min="-90" max="90" step="1" [ngModel]="w.angle" (ngModelChange)="setWm('angle', +$event)" />
97
- <span class="muted">{{ w.angle }}°</span>
98
- </div>
99
- </label>
100
- <div class="grid2">
101
- <label class="prop-row"><span>Layer</span>
102
- <div class="prop-value">
103
- <select [ngModel]="w.layer" (ngModelChange)="setWm('layer', $event)">
104
- <option value="front">In front</option>
105
- <option value="behind">Behind</option>
106
- </select>
107
- </div>
108
- </label>
109
- <label class="prop-row"><span>Pages</span>
110
- <div class="prop-value">
111
- <select [ngModel]="w.pages" (ngModelChange)="setWm('pages', $event)">
112
- <option value="all">All</option>
113
- <option value="first">First only</option>
114
- </select>
115
- </div>
116
- </label>
117
- </div>
118
- }
119
- </section>
120
-
121
- <section>
122
- @let b = background();
123
- <h4>Background image</h4>
124
- <label class="check"><input type="checkbox" [ngModel]="b.enabled" (ngModelChange)="setBg('enabled', $event)" /> Page background</label>
125
- @if (b.enabled) {
126
- <rpt-image-picker [value]="b.image" placeholder="https://…/letterhead.png" (valueChange)="setBg('image', $event)" />
127
- <div class="grid2">
128
- <label class="prop-row"><span>Fit</span>
129
- <div class="prop-value">
130
- <select [ngModel]="b.fit" (ngModelChange)="setBg('fit', $event)">
131
- <option value="cover">Cover page</option>
132
- <option value="contain">Contain</option>
133
- <option value="stretch">Stretch</option>
134
- <option value="center">Center (actual size)</option>
135
- </select>
136
- </div>
137
- </label>
138
- <label class="prop-row"><span>Pages</span>
139
- <div class="prop-value">
140
- <select [ngModel]="b.pages" (ngModelChange)="setBg('pages', $event)">
141
- <option value="all">All</option>
142
- <option value="first">First only</option>
143
- </select>
144
- </div>
145
- </label>
146
- </div>
147
- <p class="hint">Fills the whole sheet behind everything (e.g. a letterhead). For a see-through mark, use Watermark.</p>
148
- }
149
- </section>
150
- `,
151
- })
152
- export class ReportExtrasComponent {
153
- private readonly store = inject(DesignerStore);
154
- protected readonly types: ParameterType[] = ['string', 'number', 'date', 'boolean'];
155
- protected readonly parameters = computed(() => this.store.report().parameters ?? []);
156
- protected readonly filter = computed(() => this.store.report().filter ?? '');
157
- protected readonly watermark = computed(() => ({ ...defaultWatermark(), ...this.store.report().watermark }));
158
- protected readonly background = computed(() => ({ ...defaultBackground(), ...this.store.report().background }));
159
-
160
- /** Parameter names: letters, digits and _ only (they are used as @Name). */
161
- protected clean(v: string): string {
162
- return v.replace(/[^\wÀ-￿]/g, '');
163
- }
164
-
165
- protected addParam(): void {
166
- this.store.update((r) => {
167
- const n = (r.parameters?.length ?? 0) + 1;
168
- r.parameters = [...(r.parameters ?? []), { name: `Param${n}`, label: '', type: 'string', defaultValue: '', options: '' }];
169
- });
170
- }
171
-
172
- protected setParam<K extends keyof ReportParameter>(i: number, k: K, v: ReportParameter[K]): void {
173
- this.store.update((r) => {
174
- const p = r.parameters?.[i];
175
- if (p) p[k] = v;
176
- });
177
- }
178
-
179
- protected removeParam(i: number): void {
180
- this.store.update((r) => r.parameters?.splice(i, 1));
181
- }
182
-
183
- protected setFilter(v: string): void {
184
- this.store.update((r) => (r.filter = v || undefined));
185
- }
186
-
187
- protected setWm<K extends keyof Watermark>(k: K, v: Watermark[K]): void {
188
- this.store.update((r) => (r.watermark = { ...defaultWatermark(), ...r.watermark, [k]: v }));
189
- }
190
-
191
- protected num(v: number | null, k: 'fontSize' | 'imageScale', min: number): void {
192
- if (v != null && Number.isFinite(v)) this.setWm(k, Math.max(min, v));
193
- }
194
-
195
- protected setBg<K extends keyof PageBackground>(k: K, v: PageBackground[K]): void {
196
- this.store.update((r) => (r.background = { ...defaultBackground(), ...r.background, [k]: v }));
197
- }
198
- }