]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | import { |
2 | Component, | |
3 | EventEmitter, | |
4 | Input, | |
5 | OnChanges, | |
6 | OnInit, | |
7 | Output, | |
8 | ViewChild | |
9 | } from '@angular/core'; | |
10 | ||
11 | import * as _ from 'lodash'; | |
12 | ||
13 | import { CellTemplate } from '../../enum/cell-template.enum'; | |
14 | import { CdTableColumn } from '../../models/cd-table-column'; | |
81eedcae | 15 | import { CdDatePipe } from '../../pipes/cd-date.pipe'; |
11fdf7f2 TL |
16 | import { TableComponent } from '../table/table.component'; |
17 | ||
92f5a8d4 | 18 | interface KeyValueItem { |
11fdf7f2 TL |
19 | key: string; |
20 | value: any; | |
21 | } | |
22 | ||
23 | /** | |
24 | * Display the given data in a 2 column data table. The left column | |
25 | * shows the 'key' attribute, the right column the 'value' attribute. | |
26 | * The data table has the following characteristics: | |
27 | * - No header and footer is displayed | |
28 | * - The relation of the width for the columns 'key' and 'value' is 1:3 | |
29 | * - The 'key' column is displayed in bold text | |
30 | */ | |
31 | @Component({ | |
32 | selector: 'cd-table-key-value', | |
33 | templateUrl: './table-key-value.component.html', | |
34 | styleUrls: ['./table-key-value.component.scss'] | |
35 | }) | |
36 | export class TableKeyValueComponent implements OnInit, OnChanges { | |
9f95a23c | 37 | @ViewChild(TableComponent, { static: true }) |
11fdf7f2 TL |
38 | table: TableComponent; |
39 | ||
40 | @Input() | |
41 | data: any; | |
42 | @Input() | |
43 | autoReload: any = 5000; | |
44 | @Input() | |
45 | renderObjects = false; | |
46 | // Only used if objects are rendered | |
47 | @Input() | |
48 | appendParentKey = true; | |
49 | @Input() | |
50 | hideEmpty = false; | |
9f95a23c TL |
51 | @Input() |
52 | hideKeys: string[] = []; // Keys of pairs not to be displayed | |
11fdf7f2 TL |
53 | |
54 | // If set, the classAddingTpl is used to enable different css for different values | |
55 | @Input() | |
9f95a23c | 56 | customCss?: { [css: string]: number | string | ((any: any) => boolean) }; |
11fdf7f2 TL |
57 | |
58 | columns: Array<CdTableColumn> = []; | |
92f5a8d4 | 59 | tableData: KeyValueItem[]; |
11fdf7f2 TL |
60 | |
61 | /** | |
62 | * The function that will be called to update the input data. | |
63 | */ | |
64 | @Output() | |
65 | fetchData = new EventEmitter(); | |
66 | ||
81eedcae | 67 | constructor(private datePipe: CdDatePipe) {} |
11fdf7f2 TL |
68 | |
69 | ngOnInit() { | |
70 | this.columns = [ | |
71 | { | |
72 | prop: 'key', | |
73 | flexGrow: 1, | |
74 | cellTransformation: CellTemplate.bold | |
75 | }, | |
76 | { | |
77 | prop: 'value', | |
78 | flexGrow: 3 | |
79 | } | |
80 | ]; | |
81 | if (this.customCss) { | |
82 | this.columns[1].cellTransformation = CellTemplate.classAdding; | |
83 | } | |
84 | // We need to subscribe the 'fetchData' event here and not in the | |
85 | // HTML template, otherwise the data table will display the loading | |
86 | // indicator infinitely if data is only bound via '[data]="xyz"'. | |
87 | // See for 'loadingIndicator' in 'TableComponent::ngOnInit()'. | |
88 | if (this.fetchData.observers.length > 0) { | |
89 | this.table.fetchData.subscribe(() => { | |
90 | // Forward event triggered by the 'cd-table' data table. | |
91 | this.fetchData.emit(); | |
92 | }); | |
93 | } | |
94 | this.useData(); | |
95 | } | |
96 | ||
97 | ngOnChanges() { | |
98 | this.useData(); | |
99 | } | |
100 | ||
101 | useData() { | |
102 | if (!this.data) { | |
103 | return; // Wait for data | |
104 | } | |
9f95a23c TL |
105 | let pairs = this.makePairs(this.data); |
106 | if (this.hideKeys) { | |
107 | pairs = pairs.filter((pair) => !this.hideKeys.includes(pair.key)); | |
108 | } | |
109 | this.tableData = pairs; | |
11fdf7f2 TL |
110 | } |
111 | ||
92f5a8d4 TL |
112 | private makePairs(data: any): KeyValueItem[] { |
113 | let result: KeyValueItem[] = []; | |
11fdf7f2 | 114 | if (!data) { |
9f95a23c | 115 | return undefined; // Wait for data |
11fdf7f2 | 116 | } else if (_.isArray(data)) { |
92f5a8d4 | 117 | result = this.makePairsFromArray(data); |
11fdf7f2 | 118 | } else if (_.isObject(data)) { |
92f5a8d4 | 119 | result = this.makePairsFromObject(data); |
11fdf7f2 TL |
120 | } else { |
121 | throw new Error('Wrong data format'); | |
122 | } | |
92f5a8d4 TL |
123 | result = result |
124 | .map((item) => { | |
125 | item.value = this.convertValue(item.value); | |
126 | return item; | |
127 | }) | |
128 | .filter((i) => i.value !== null); | |
129 | return _.sortBy(this.renderObjects ? this.insertFlattenObjects(result) : result, 'key'); | |
11fdf7f2 TL |
130 | } |
131 | ||
92f5a8d4 | 132 | private makePairsFromArray(data: any[]): KeyValueItem[] { |
9f95a23c | 133 | let temp: any[] = []; |
11fdf7f2 TL |
134 | const first = data[0]; |
135 | if (_.isArray(first)) { | |
136 | if (first.length === 2) { | |
137 | temp = data.map((a) => ({ | |
138 | key: a[0], | |
139 | value: a[1] | |
140 | })); | |
141 | } else { | |
92f5a8d4 TL |
142 | throw new Error( |
143 | `Array contains too many elements (${first.length}). ` + | |
144 | `Needs to be of type [string, any][]` | |
145 | ); | |
11fdf7f2 TL |
146 | } |
147 | } else if (_.isObject(first)) { | |
148 | if (_.has(first, 'key') && _.has(first, 'value')) { | |
149 | temp = [...data]; | |
150 | } else { | |
151 | temp = data.reduce( | |
92f5a8d4 | 152 | (previous: any[], item) => previous.concat(this.makePairsFromObject(item)), |
11fdf7f2 TL |
153 | temp |
154 | ); | |
155 | } | |
156 | } | |
157 | return temp; | |
158 | } | |
159 | ||
9f95a23c | 160 | private makePairsFromObject(data: any): KeyValueItem[] { |
11fdf7f2 TL |
161 | return Object.keys(data).map((k) => ({ |
162 | key: k, | |
163 | value: data[k] | |
164 | })); | |
165 | } | |
166 | ||
92f5a8d4 | 167 | private insertFlattenObjects(data: KeyValueItem[]): any[] { |
11fdf7f2 | 168 | return _.flattenDeep( |
92f5a8d4 | 169 | data.map((item) => { |
11fdf7f2 TL |
170 | const value = item.value; |
171 | const isObject = _.isObject(value); | |
172 | if (!isObject || _.isEmpty(value)) { | |
173 | if (isObject) { | |
174 | item.value = ''; | |
175 | } | |
176 | return item; | |
177 | } | |
178 | return this.splitItemIntoItems(item); | |
179 | }) | |
180 | ); | |
181 | } | |
182 | ||
183 | /** | |
184 | * Split item into items will call _makePairs inside _makePairs (recursion), in oder to split | |
185 | * the object item up into items as planned. | |
186 | */ | |
92f5a8d4 TL |
187 | private splitItemIntoItems(data: { key: string; value: object }): KeyValueItem[] { |
188 | return this.makePairs(data.value).map((item) => { | |
11fdf7f2 | 189 | if (this.appendParentKey) { |
92f5a8d4 | 190 | item.key = data.key + ' ' + item.key; |
11fdf7f2 TL |
191 | } |
192 | return item; | |
193 | }); | |
194 | } | |
195 | ||
92f5a8d4 TL |
196 | private convertValue(value: any): KeyValueItem { |
197 | if (_.isArray(value)) { | |
198 | if (_.isEmpty(value) && this.hideEmpty) { | |
199 | return null; | |
200 | } | |
201 | value = value.map((item) => (_.isObject(item) ? JSON.stringify(item) : item)).join(', '); | |
202 | } else if (_.isObject(value)) { | |
203 | if ((this.hideEmpty && _.isEmpty(value)) || !this.renderObjects) { | |
204 | return null; | |
205 | } | |
206 | } else if (_.isString(value)) { | |
207 | if (value === '' && this.hideEmpty) { | |
208 | return null; | |
209 | } | |
210 | if (this.isDate(value)) { | |
211 | value = this.datePipe.transform(value) || value; | |
212 | } | |
81eedcae | 213 | } |
81eedcae | 214 | |
92f5a8d4 | 215 | return value; |
81eedcae TL |
216 | } |
217 | ||
9f95a23c | 218 | private isDate(s: string) { |
81eedcae TL |
219 | const sep = '[ -:.TZ]'; |
220 | const n = '\\d{2}' + sep; | |
221 | // year - m - d - h : m : s . someRest Z (if UTC) | |
222 | return s.match(new RegExp('^\\d{4}' + sep + n + n + n + n + n + '\\d*' + 'Z?$')); | |
223 | } | |
11fdf7f2 | 224 | } |