@onehat/data 1.21.19 → 1.21.21

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.
@@ -0,0 +1,458 @@
1
+ /** @module Property */
2
+
3
+ import EventEmitter from '@onehat/events';
4
+ import Base64Property from './Base64.js';
5
+ import BooleanProperty from './Boolean.js';
6
+ import CurrencyProperty from './Currency.js';
7
+ import DateProperty from './Date.js';
8
+ import DateTimeProperty from './DateTime.js';
9
+ import FileProperty from './File.js';
10
+ import FloatProperty from './Float.js';
11
+ import IntegerProperty from './Integer.js';
12
+ import JsonProperty, { TagProperty } from './Json.js';
13
+ import PercentProperty from './Percent.js';
14
+ import PercentIntProperty from './PercentInt.js';
15
+ import StringProperty from './String.js';
16
+ import TimeProperty from './Time.js';
17
+ import UuidProperty from './Uuid.js';
18
+ import _ from 'lodash';
19
+
20
+ /**
21
+ * Class represents a Property that can store values of multiple types.
22
+ * The actual type is determined dynamically based on the value being set.
23
+ *
24
+ * Usage: { name: 'date', title: 'Date', type: 'mixed', types: ['date', 'string'], },
25
+ * This is primarily used to allow a field that's normally one PropertyType
26
+ * to also accept string values (e.g. values like '2025-01-01' or 'N/A').
27
+ *
28
+ * @extends Property
29
+ */
30
+ export default class MixedProperty extends EventEmitter {
31
+
32
+ constructor(config = {}, entity) {
33
+ config = _.merge({}, MixedProperty.defaults, config);
34
+
35
+ if (!config.types || !Array.isArray(config.types) || config.types.length < 2) {
36
+ throw Error('MixedProperty requires a types array with at least two types in its configuration.');
37
+ }
38
+
39
+ super(config, entity);
40
+
41
+ this.registerEvents([
42
+ 'change',
43
+ 'changeValidity',
44
+ 'destroy',
45
+ ]);
46
+
47
+ this.types = config.types;
48
+ this.currentType = this.types[0].type || this.types[0];
49
+ this.internalProperties = new Map();
50
+
51
+ this._createInternalProperties();
52
+
53
+ this.currentProperty = this.internalProperties.get(this.currentType);
54
+
55
+ this._proxy = new Proxy(this, {
56
+ get(mixedProperty, prop, receiver) {
57
+ if (prop in mixedProperty) {
58
+ const value = mixedProperty[prop];
59
+ if (typeof value === 'function') {
60
+ return value.bind(mixedProperty);
61
+ }
62
+ return value;
63
+ }
64
+ if (mixedProperty.currentProperty && prop in mixedProperty.currentProperty) {
65
+ const value = mixedProperty.currentProperty[prop];
66
+ if (typeof value === 'function') {
67
+ return value.bind(mixedProperty.currentProperty);
68
+ }
69
+ return value;
70
+ }
71
+ return undefined;
72
+ },
73
+ has(mixedProperty, prop) {
74
+ if (prop in mixedProperty) {
75
+ return true;
76
+ }
77
+ return mixedProperty.currentProperty ? prop in mixedProperty.currentProperty : false;
78
+ }
79
+ });
80
+
81
+ return this._proxy;
82
+ }
83
+
84
+ /**
85
+ * Creates internal Property instances for each configured type
86
+ * @private
87
+ */
88
+ _createInternalProperties() {
89
+ _.each(this.types, (typeConfig) => {
90
+ let typeName,
91
+ propertyConfig;
92
+ if (typeof typeConfig === 'string') {
93
+ typeName = typeConfig;
94
+ propertyConfig = {};
95
+ } else {
96
+ typeName = typeConfig.type;
97
+ propertyConfig = _.omit(typeConfig, 'type');
98
+ }
99
+
100
+ const
101
+ PropertyClass = this._getPropertyClass(typeName),
102
+
103
+ // Create a clean config for this internal property
104
+ baseConfig = _.omit(this.config, ['types', 'defaultType']), // Start with base config but exclude Mixed-specific settings
105
+ mergedConfig = _.merge({}, baseConfig, propertyConfig, { // Merge with type-specific config, giving precedence to type-specific settings
106
+ name: this.name,
107
+ }),
108
+ property = new PropertyClass(mergedConfig, this.entity);
109
+
110
+ this._setupEventForwarding(property);
111
+
112
+ this.internalProperties.set(typeName, property);
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Gets the Property class for a given type name
118
+ * @param {string} typeName - Name of the property type
119
+ * @returns {Class} Property class
120
+ * @private
121
+ */
122
+ _getPropertyClass(typeName) {
123
+ const
124
+ typeMap = {
125
+ base64: Base64Property,
126
+ bool: BooleanProperty,
127
+ currency: CurrencyProperty,
128
+ date: DateProperty,
129
+ datetime: DateTimeProperty,
130
+ file: FileProperty,
131
+ float: FloatProperty,
132
+ int: IntegerProperty,
133
+ json: JsonProperty,
134
+ percent: PercentProperty,
135
+ percentint: PercentIntProperty,
136
+ string: StringProperty,
137
+ tag: TagProperty,
138
+ time: TimeProperty,
139
+ uuid: UuidProperty,
140
+ },
141
+ PropertyClass = typeMap[typeName.toLowerCase()];
142
+
143
+ if (!PropertyClass) {
144
+ const availableTypes = Object.keys(typeMap).join(', ');
145
+ throw new Error(`Unknown property type: '${typeName}'. Available types are: ${availableTypes}`);
146
+ }
147
+
148
+ return typeMap[typeName.toLowerCase()];
149
+ }
150
+
151
+ /**
152
+ * Sets up event forwarding from an internal property to this MixedProperty
153
+ * @param {Property} property - Internal property to forward events from
154
+ * @private
155
+ */
156
+ _setupEventForwarding(property) {
157
+ // Forward all events from internal property to MixedProperty
158
+ const forwardEvent = (eventName, ...args) => {
159
+ if (property === this.currentProperty) {
160
+ // Replace the property reference in args with this MixedProperty
161
+ const modifiedArgs = args.map(arg => arg === property ? this : arg);
162
+ this.emit(eventName, ...modifiedArgs);
163
+ }
164
+ };
165
+
166
+ // Listen to all registered events on the internal property
167
+ property.getRegisteredEvents().forEach((eventName) => {
168
+ property.on(eventName, (...args) => forwardEvent(eventName, ...args));
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Detects the appropriate property type for a given value
174
+ * @param {any} value - Value to analyze
175
+ * @returns {string} Detected type name
176
+ * @private
177
+ */
178
+ _detectType(value) {
179
+ if (_.isNil(value)) {
180
+ return this.currentType || this.defaultType || (typeof this.types[0] === 'string' ? this.types[0] : this.types[0].type);
181
+ }
182
+
183
+ // Get available type names from config
184
+ const availableTypes = this.types.map(typeConfig =>
185
+ typeof typeConfig === 'string' ? typeConfig : typeConfig.type
186
+ );
187
+
188
+ // Define precedence order - more specific types first
189
+ const precedenceOrder = [
190
+ 'bool', // Most specific - only true/false/1/0
191
+ 'int', // Integers
192
+ 'float', // Floating point numbers
193
+ 'currency', // Currency values
194
+ 'percent', // Percentage values
195
+ 'percentint',// Integer percentages
196
+ 'date', // Date values
197
+ 'datetime', // DateTime values
198
+ 'time', // Time values
199
+ 'uuid', // UUID format
200
+ 'base64', // Base64 encoded data
201
+ 'json', // JSON objects/arrays
202
+ 'tag', // Tag format
203
+ 'file', // File data
204
+ 'string' // Most general - accepts anything
205
+ ];
206
+
207
+ // Try types in precedence order, but only if they're in the available types
208
+ let typeName;
209
+ for(typeName of precedenceOrder) {
210
+ if (availableTypes.includes(typeName)) {
211
+ const property = this.internalProperties.get(typeName);
212
+ if (property && this._canParseValue(property, value)) {
213
+ return typeName;
214
+ }
215
+ }
216
+ }
217
+
218
+ throw new Error(`No configured property type could handle value: ${value}. Available types: ${availableTypes.join(', ')}`);
219
+ }
220
+
221
+ /**
222
+ * Tests if a property can successfully parse a value
223
+ * @param {Property} property - Property to test
224
+ * @param {any} value - Value to test
225
+ * @returns {boolean} Whether the property can parse the value
226
+ * @private
227
+ */
228
+ _canParseValue(property, value) {
229
+ try {
230
+ switch(property.constructor.type) {
231
+ case 'bool':
232
+ return this._isBooleanValue(value);
233
+ case 'int':
234
+ return this._isIntegerValue(value);
235
+ case 'float':
236
+ return this._isFloatValue(value);
237
+ }
238
+
239
+ property.parse(value);
240
+ return true;
241
+ } catch (error) {
242
+ return false;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Checks if a value represents a boolean
248
+ * @param {any} value - Value to check
249
+ * @returns {boolean} Whether the value is boolean-like
250
+ * @private
251
+ */
252
+ _isBooleanValue(value) {
253
+ switch(typeof value) {
254
+ case 'boolean':
255
+ return true;
256
+ case 'number':
257
+ return value === 0 || value === 1;
258
+ case 'string':
259
+ const lower = value.toLowerCase().trim();
260
+ return ['true', 'false', '1', '0'].includes(lower);
261
+ default:
262
+ return false;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Checks if a value represents an integer
268
+ * @param {any} value - Value to check
269
+ * @returns {boolean} Whether the value is integer-like
270
+ * @private
271
+ */
272
+ _isIntegerValue(value) {
273
+ switch(typeof value) {
274
+ case 'bigint':
275
+ return true;
276
+ case 'number':
277
+ return Number.isInteger(value);
278
+ case 'string':
279
+ const trimmed = value.trim();
280
+ if (trimmed === '') {
281
+ return false;
282
+ }
283
+ const num = Number(trimmed);
284
+ return !isNaN(num) && Number.isInteger(num) && String(num) === trimmed;
285
+ }
286
+ return false;
287
+ }
288
+
289
+ /**
290
+ * Checks if a value represents a float
291
+ * @param {any} value - Value to check
292
+ * @returns {boolean} Whether the value is float-like
293
+ * @private
294
+ */
295
+ _isFloatValue(value) {
296
+ switch(typeof value) {
297
+ case 'number':
298
+ return !Number.isInteger(value) && isFinite(value);
299
+ case 'string':
300
+ const trimmed = value.trim();
301
+ if (trimmed === '') return false;
302
+ const num = Number(trimmed);
303
+ return !isNaN(num) && isFinite(num) && !Number.isInteger(num);
304
+ }
305
+ return false;
306
+ }
307
+
308
+ /**
309
+ * Sets the value and determines the appropriate type
310
+ * @param {any} value - Value to set
311
+ */
312
+ setValue(value) {
313
+ const detectedType = this._detectType(value);
314
+ if (this.currentType !== detectedType) {
315
+ this.currentType = detectedType;
316
+ this.currentProperty = this.internalProperties.get(this.currentType);
317
+ }
318
+ this.currentProperty.setValue(value);
319
+ }
320
+
321
+ /**
322
+ * Gets the current type name
323
+ * @returns {string} Current type name
324
+ */
325
+ getCurrentType() {
326
+ return this.currentProperty.constructor.type;
327
+ }
328
+
329
+ /**
330
+ * Gets the className of this Property type.
331
+ * @return {string} className
332
+ */
333
+ getClassName() {
334
+ return this.constructor.className;
335
+ }
336
+
337
+ /**
338
+ * Gets the internal property for a specific type
339
+ * @param {string} typeName - Type name
340
+ * @returns {Property} Internal property
341
+ */
342
+ getInternalProperty(typeName) {
343
+ return this.internalProperties.get(typeName);
344
+ }
345
+
346
+ // ______ __
347
+ // / ____/ _____ ____ / /______
348
+ // / __/ | | / / _ \/ __ \/ __/ ___/
349
+ // / /___ | |/ / __/ / / / /_(__ )
350
+ // /_____/ |___/\___/_/ /_/\__/____/
351
+ //
352
+ // (Override the EventEmitter methods to forward listener management to internal properties)
353
+
354
+
355
+ /**
356
+ * Add event listener to MixedProperty only
357
+ * Events from internal properties are forwarded via _setupEventForwarding
358
+ */
359
+ on(eventName, listener) {
360
+ return super.on(eventName, listener);
361
+ }
362
+
363
+ /**
364
+ * Add one-time event listener to MixedProperty only
365
+ */
366
+ once(eventName, listener) {
367
+ return super.once(eventName, listener);
368
+ }
369
+
370
+ /**
371
+ * Remove event listener from MixedProperty only
372
+ */
373
+ off(eventName, listener) {
374
+ return super.off(eventName, listener);
375
+ }
376
+
377
+ /**
378
+ * Remove all event listeners from MixedProperty and all internal properties
379
+ */
380
+ removeAllListeners(eventName) {
381
+ // Remove all listeners from this MixedProperty
382
+ super.removeAllListeners(eventName);
383
+
384
+ // Remove all listeners from internal properties
385
+ this.internalProperties.forEach(property => {
386
+ property.removeAllListeners(eventName);
387
+ });
388
+
389
+ return this;
390
+ }
391
+
392
+ /**
393
+ * Destroys all internal properties
394
+ */
395
+ destroy() {
396
+ // Clean up listener mappings
397
+ if (this._listenerMappings) {
398
+ this._listenerMappings.clear();
399
+ this._listenerMappings = null;
400
+ }
401
+
402
+ this.internalProperties.forEach((property) => property.destroy());
403
+ this.internalProperties.clear();
404
+ }
405
+
406
+ /**
407
+ * Returns the default configuration for this PropertyType.
408
+ * For MixedProperty, this delegates to the first configured type's defaults.
409
+ * @param {Object} defaults - The default configuration to merge with (should include 'types' array)
410
+ * @returns {Object} The default configuration
411
+ */
412
+ static getStaticDefaults(defaults = {}) {
413
+ const mixedPropertyDefaults = {
414
+ name: null,
415
+ allowNull: true,
416
+ depends: null,
417
+ mapping: null,
418
+ submitAsString: false,
419
+ isSortable: true,
420
+ isTempId: false,
421
+ isVirtual: false,
422
+ title: null,
423
+ tooltip: null,
424
+ fieldGroup: null,
425
+ isForeignModel: false,
426
+ filterType: null,
427
+ isFilteringDisabled: false,
428
+ viewerType: null,
429
+ editorType: null,
430
+ isEditingDisabled: false,
431
+ defaultValue: null,
432
+ formatter: null,
433
+ };
434
+
435
+ // If types are configured, use the first type's default value
436
+ if (defaults.types && Array.isArray(defaults.types) && defaults.types.length > 0) {
437
+ const
438
+ firstTypeConfig = defaults.types[0],
439
+ firstTypeName = typeof firstTypeConfig === 'string' ? firstTypeConfig : firstTypeConfig.type;
440
+ if (firstTypeName) {
441
+ try {
442
+ const PropertyClass = MixedProperty.prototype._getPropertyClass(firstTypeName);
443
+ const typeDefaults = PropertyClass.getStaticDefaults ? PropertyClass.getStaticDefaults() : {};
444
+ if (typeDefaults.defaultValue !== undefined) {
445
+ mixedPropertyDefaults.defaultValue = typeDefaults.defaultValue;
446
+ }
447
+ } catch (e) {
448
+ // If we can't get the property class, just use null default
449
+ }
450
+ }
451
+ }
452
+
453
+ return _.merge({}, defaults);
454
+ }
455
+ }
456
+
457
+ MixedProperty.className = 'Mixed';
458
+ MixedProperty.type = 'mixed';
@@ -1,6 +1,7 @@
1
1
  /** @module Property */
2
2
 
3
3
  import EventEmitter from '@onehat/events';
4
+ import Formatters from '../Util/Formatters.js';
4
5
  import _ from 'lodash';
5
6
 
6
7
  /**
@@ -137,6 +138,12 @@ export default class Property extends EventEmitter {
137
138
  */
138
139
  defaultValue: null,
139
140
 
141
+ /**
142
+ * @member {string} formatter - The name of the formatter to use for this property
143
+ * @private
144
+ */
145
+ formatter: null,
146
+
140
147
  };
141
148
 
142
149
  /**
@@ -278,6 +285,9 @@ export default class Property extends EventEmitter {
278
285
  if (this.isDestroyed) {
279
286
  throw Error('this.getDisplayValue is no longer valid. Property has been destroyed.');
280
287
  }
288
+ if (this.formatter) {
289
+ return Formatters[this.formatter](this.parsedValue);
290
+ }
281
291
  return this.parsedValue;
282
292
  }
283
293
 
@@ -428,6 +438,17 @@ export default class Property extends EventEmitter {
428
438
  return value;
429
439
  }
430
440
 
441
+ /**
442
+ * Sets the formatter for this Property.
443
+ * @param {*} formatter
444
+ */
445
+ setFormatter(formatter) {
446
+ if (this.isDestroyed) {
447
+ throw Error('this.setFormatter is no longer valid. Property has been destroyed.');
448
+ }
449
+ this.formatter = formatter;
450
+ }
451
+
431
452
 
432
453
 
433
454
 
@@ -9,6 +9,7 @@ import FileProperty from './File.js';
9
9
  import FloatProperty from './Float.js';
10
10
  import IntegerProperty from './Integer.js';
11
11
  import JsonProperty, { TagProperty } from './Json.js';
12
+ import MixedProperty from './Mixed.js';
12
13
  import PercentProperty from './Percent.js';
13
14
  import PercentIntProperty from './PercentInt.js';
14
15
  import Property from './Property.js';
@@ -26,6 +27,7 @@ const PropertyTypes = {
26
27
  [FloatProperty.type]: FloatProperty,
27
28
  [IntegerProperty.type]: IntegerProperty,
28
29
  [JsonProperty.type]: JsonProperty,
30
+ [MixedProperty.type]: MixedProperty,
29
31
  [PercentProperty.type]: PercentProperty,
30
32
  [PercentIntProperty.type]: PercentIntProperty,
31
33
  [Property.type]: Property,
@@ -1,10 +1,10 @@
1
1
  /** @module Reader */
2
2
 
3
3
  import JsonReader from './JsonReader.js';
4
- import XmlReader from './XmlReader.js';
4
+ // import XmlReader from './XmlReader.js';
5
5
 
6
6
  const ReaderTypes = {
7
7
  [JsonReader.type]: JsonReader,
8
- [XmlReader.type]: XmlReader,
8
+ // [XmlReader.type]: XmlReader,
9
9
  };
10
10
  export default ReaderTypes;