alchemymvc 1.4.0-alpha.9 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/app/behaviour/sluggable_behaviour.js +72 -1
  2. package/lib/app/conduit/electron_conduit.js +1 -1
  3. package/lib/app/conduit/socket_conduit.js +62 -8
  4. package/lib/app/controller/alchemy_info_controller.js +2 -2
  5. package/lib/app/datasource/mongo_datasource.js +84 -104
  6. package/lib/app/element/al_time.js +126 -0
  7. package/lib/app/helper/enum_values.js +9 -0
  8. package/lib/app/helper/router_helper.js +37 -5
  9. package/lib/app/helper/socket_helper.js +71 -14
  10. package/lib/app/helper/syncable.js +253 -30
  11. package/lib/app/helper_datasource/00-nosql_datasource.js +403 -53
  12. package/lib/app/helper_datasource/05-fallback_datasource.js +5 -2
  13. package/lib/app/helper_datasource/idb_datasource.js +75 -59
  14. package/lib/app/helper_datasource/indexed_db.js +41 -33
  15. package/lib/app/helper_datasource/read_operational_context.js +0 -17
  16. package/lib/app/helper_field/00-objectid_field.js +16 -0
  17. package/lib/app/helper_field/11-date_field.js +13 -0
  18. package/lib/app/helper_field/datetime_field.js +13 -0
  19. package/lib/app/helper_field/enum_field.js +27 -0
  20. package/lib/app/helper_field/geopoint_field.js +61 -0
  21. package/lib/app/helper_field/html_field.js +14 -1
  22. package/lib/app/helper_field/integer_field.js +14 -0
  23. package/lib/app/helper_field/local_date_field.js +13 -0
  24. package/lib/app/helper_field/local_date_time_field.js +13 -0
  25. package/lib/app/helper_field/local_time_field.js +13 -0
  26. package/lib/app/helper_field/password_field.js +24 -0
  27. package/lib/app/helper_field/schema_field.js +85 -2
  28. package/lib/app/helper_field/url_field.js +22 -0
  29. package/lib/app/helper_model/00-base_criteria.js +67 -6
  30. package/lib/app/helper_model/05-criteria_expressions.js +23 -4
  31. package/lib/app/helper_model/10-model_criteria.js +39 -18
  32. package/lib/app/helper_model/document.js +12 -7
  33. package/lib/app/helper_model/field_config.js +9 -2
  34. package/lib/app/helper_model/model.js +9 -8
  35. package/lib/app/model/system_task_history_model.js +11 -1
  36. package/lib/class/conduit.js +112 -12
  37. package/lib/class/datasource.js +30 -2
  38. package/lib/class/document.js +1 -1
  39. package/lib/class/field.js +170 -7
  40. package/lib/class/inode_file.js +2 -2
  41. package/lib/class/model.js +12 -11
  42. package/lib/class/operational_context.js +37 -0
  43. package/lib/class/route.js +34 -3
  44. package/lib/class/router.js +14 -7
  45. package/lib/class/schema.js +1 -1
  46. package/lib/class/schema_client.js +141 -14
  47. package/lib/class/session.js +1 -1
  48. package/lib/class/task.js +30 -1
  49. package/lib/class/task_service.js +83 -14
  50. package/lib/core/alchemy.js +78 -4
  51. package/lib/core/alchemy_functions.js +7 -11
  52. package/lib/core/alchemy_load_functions.js +37 -5
  53. package/lib/core/client_alchemy.js +9 -1
  54. package/lib/core/middleware.js +52 -20
  55. package/lib/core/setting.js +75 -6
  56. package/lib/scripts/create_settings.js +44 -0
  57. package/lib/scripts/setup_ai_devmode.js +324 -0
  58. package/lib/scripts/setup_devwatch.js +0 -0
  59. package/lib/stages/00-load_core.js +1 -1
  60. package/lib/stages/50-routes.js +6 -1
  61. package/lib/stages/90-server.js +0 -1
  62. package/package.json +18 -18
@@ -282,15 +282,86 @@ Sluggable.setMethod(function createSlug(record, new_value, callback) {
282
282
 
283
283
  return Function.parallel(tasks, callback);
284
284
  } else {
285
+ let existing_slug = record[that.target_field.name];
286
+
285
287
  // Only generate a new slug if it doesn't exist yet
286
- if (!record[that.target_field.name]) {
288
+ if (!existing_slug) {
287
289
  return that.generateSlug(record[that.source_field.name], false, record, callback);
288
290
  }
291
+
292
+ // Check if the existing slug is a duplicate (belongs to another record)
293
+ return that.checkSlugIsDuplicate(existing_slug, false, record, function checked(err, is_duplicate) {
294
+ if (err) {
295
+ return callback(err);
296
+ }
297
+
298
+ // If the slug is a duplicate, regenerate from the source field
299
+ if (is_duplicate) {
300
+ return that.generateSlug(record[that.source_field.name], false, record, callback);
301
+ }
302
+
303
+ // Slug is not a duplicate, keep it
304
+ callback(null, existing_slug);
305
+ });
289
306
  }
290
307
 
291
308
  callback();
292
309
  });
293
310
 
311
+ /**
312
+ * Check if a slug already exists (belongs to another record)
313
+ *
314
+ * @author Jelle De Loecker <jelle@elevenways.be>
315
+ * @since 1.4.0
316
+ * @version 1.4.0
317
+ *
318
+ * @param {string} slug
319
+ * @param {string} key The translation key (or false)
320
+ * @param {Document} record
321
+ * @param {Function} callback
322
+ */
323
+ Sluggable.setMethod(function checkSlugIsDuplicate(slug, key, record, callback) {
324
+
325
+ let that = this,
326
+ for_record_id = record?._id,
327
+ model = Model.get(this.model.name),
328
+ path = this.target_field.name;
329
+
330
+ if (key) {
331
+ path += '.' + key;
332
+ }
333
+
334
+ let conditions = {};
335
+ conditions[path] = slug;
336
+
337
+ // Also include unique_modifier_fields in the check
338
+ if (this.unique_modifier_fields.length && record) {
339
+ for (let i = 0; i < this.unique_modifier_fields.length; i++) {
340
+ let field = this.unique_modifier_fields[i];
341
+ conditions[field] = record[field];
342
+ }
343
+ }
344
+
345
+ model.find('first', {conditions: conditions}, function gotFirst(err, found_item) {
346
+ if (err) {
347
+ return callback(err);
348
+ }
349
+
350
+ // If no item found, it's not a duplicate
351
+ if (!found_item) {
352
+ return callback(null, false);
353
+ }
354
+
355
+ // If the found item is the same as the current record, it's not a duplicate
356
+ if (for_record_id && String(for_record_id) == String(found_item._id)) {
357
+ return callback(null, false);
358
+ }
359
+
360
+ // It's a duplicate
361
+ callback(null, true);
362
+ });
363
+ });
364
+
294
365
  /**
295
366
  * Actually generate the slug from the given string,
296
367
  * and look for existing slugs in the path
@@ -80,7 +80,7 @@ ElectronConduit.setMethod(function prepareViewRender() {
80
80
  });
81
81
 
82
82
  ElectronConduit.setMethod(function setHeader(name, val) {
83
- console.log('Setting header', name, val);
83
+ // Headers are not used in Electron conduit
84
84
  });
85
85
 
86
86
  /**
@@ -6,7 +6,7 @@ var iostream = alchemy.use('socket.io-stream'),
6
6
  *
7
7
  * @author Jelle De Loecker <jelle@elevenways.be>
8
8
  * @since 0.2.0
9
- * @version 1.3.10
9
+ * @version 1.4.0
10
10
  *
11
11
  * @param {Socker} socket
12
12
  * @param {Object} announcement
@@ -25,6 +25,10 @@ var SocketConduit = Function.inherits('Alchemy.Conduit', function Socket(socket,
25
25
  // Store the announcement data
26
26
  this.announcement = announcement;
27
27
 
28
+ if (!this.canCreateSocketConnection()) {
29
+ return;
30
+ }
31
+
28
32
  // Detect node clients
29
33
  if (this.headers['user-agent'] == 'node-XMLHttpRequest') {
30
34
  this.isNodeClient = true;
@@ -84,8 +88,8 @@ var SocketConduit = Function.inherits('Alchemy.Conduit', function Socket(socket,
84
88
 
85
89
  // Listen for responses on the stream socket
86
90
  this.stream.on('response', function onStreamResponse(stream, data) {
87
- packet.stream = stream;
88
- that.onPayload(packet);
91
+ data.stream = stream;
92
+ that.onPayload(data);
89
93
  });
90
94
 
91
95
  // Listen to data subscriptions
@@ -145,6 +149,22 @@ SocketConduit.setProperty(function is_connected() {
145
149
  return this.socket?.connected || false;
146
150
  });
147
151
 
152
+ /**
153
+ * Is this client allowed to create a socket connection?
154
+ *
155
+ * @author Jelle De Loecker <jelle@elevenways.be>
156
+ * @since 1.4.0
157
+ * @version 1.4.0
158
+ */
159
+ SocketConduit.setMethod(function canCreateSocketConnection() {
160
+
161
+ if (this.isCrawler()) {
162
+ return false;
163
+ }
164
+
165
+ return true;
166
+ });
167
+
148
168
  /**
149
169
  * Parse the request, get information from the url
150
170
  *
@@ -166,7 +186,7 @@ SocketConduit.setMethod(function parseRequest() {
166
186
  *
167
187
  * @param {Object} data
168
188
  */
169
- SocketConduit.setMethod(function parseAnnouncement() {
189
+ SocketConduit.setMethod(async function parseAnnouncement() {
170
190
 
171
191
  var connections,
172
192
  data = this.announcement;
@@ -179,6 +199,10 @@ SocketConduit.setMethod(function parseAnnouncement() {
179
199
  // Register the connection in the user's session
180
200
  this.getSession().registerConnection(this);
181
201
 
202
+ // Allow plugins to restore session state (e.g., persistent login cookies)
203
+ // This is critical after server restarts when the session exists but has no user data
204
+ await alchemy.emit('restoring_websocket_session', this);
205
+
182
206
  // Tell the client we're ready
183
207
  this.websocket.emit('ready');
184
208
 
@@ -192,7 +216,7 @@ SocketConduit.setMethod(function parseAnnouncement() {
192
216
  *
193
217
  * @author Jelle De Loecker <jelle@elevenways.be>
194
218
  * @since 0.2.0
195
- * @version 0.4.0
219
+ * @version 1.4.0
196
220
  *
197
221
  * @param {Object} packet
198
222
  */
@@ -237,9 +261,39 @@ SocketConduit.setMethod(function onLinkup(packet) {
237
261
  router = Router;
238
262
  }
239
263
 
264
+ // Helper to send permission denied error and clean up
265
+ const send_permission_denied = (required_permission) => {
266
+ let error_linkup = new Linkup(this, type, id, packet.data);
267
+ error_linkup.submit('error', {
268
+ code: 'PERMISSION_DENIED',
269
+ message: 'Permission denied',
270
+ required_permission: required_permission
271
+ });
272
+ error_linkup.destroy();
273
+ };
274
+
240
275
  // See if any socket routes have been set
241
276
  if (router.linkupRoutes[type]) {
242
- fnc = router.linkupRoutes[type];
277
+ let route_config = router.linkupRoutes[type];
278
+
279
+ // Support both old format (just fnc) and new format ({fnc, permission})
280
+ if (typeof route_config === 'function' || typeof route_config === 'string') {
281
+ fnc = route_config;
282
+ } else {
283
+ fnc = route_config.fnc;
284
+
285
+ // Check the linkup route's permission
286
+ if (route_config.permission && !this.hasPermission(route_config.permission)) {
287
+ send_permission_denied(route_config.permission);
288
+ return;
289
+ }
290
+ }
291
+
292
+ // Check the router section's permissions
293
+ if (!router.checkPermission(this)) {
294
+ send_permission_denied(router.permissions);
295
+ return;
296
+ }
243
297
 
244
298
  // Create the new linkup instance
245
299
  linkup = new Linkup(this, type, id, packet.data);
@@ -335,7 +389,7 @@ SocketConduit.setMethod(function onPayload(packet) {
335
389
  let test = linkup.simpleListeners.get(packet.type);
336
390
 
337
391
  if (!test) {
338
- console.error('Linkup', linkup, 'has no listener for', packet.type);
392
+ log.warn('Linkup has no listener for', packet.type);
339
393
  }
340
394
 
341
395
  if (packet.stream) {
@@ -679,6 +733,6 @@ Linkup.setMethod(function createStream() {
679
733
  * @param {Function} callback
680
734
  */
681
735
  Linkup.setMethod(function error(err, callback) {
682
- console.log('Error:', err);
736
+ log.error('Linkup error:', err);
683
737
  this.submit('error', {stack: err.stack, message: err.message}, callback);
684
738
  });
@@ -149,8 +149,8 @@ Info.setAction(function postponed(conduit, id) {
149
149
  *
150
150
  * @author Jelle De Loecker <jelle@elevenways.be>
151
151
  * @since 1.3.10
152
- * @version 1.3.10
152
+ * @version 1.4.0
153
153
  */
154
154
  Info.setAction(function syncable(conduit, linkup, config) {
155
- Classes.Alchemy.Syncable.handleLink(conduit, linkup, config);
155
+ Classes.Alchemy.Syncable.Syncable.handleLink(conduit, linkup, config);
156
156
  });
@@ -84,7 +84,7 @@ Mongo.setSupport('querying_associations', true);
84
84
  *
85
85
  * @return {Object}
86
86
  */
87
- Mongo.setProperty('allowed_find_options', [
87
+ Mongo.setProperty('allowed_find_options', new Set([
88
88
  'limit',
89
89
  'sort',
90
90
  'projection',
@@ -111,7 +111,7 @@ Mongo.setProperty('allowed_find_options', [
111
111
  'maxTimeMS',
112
112
  'collation',
113
113
  'session'
114
- ]);
114
+ ]));
115
115
 
116
116
  /**
117
117
  * Convert the given value to a BigInt
@@ -230,7 +230,7 @@ Mongo.setMethod(function normalizeFindOptions(options) {
230
230
  }
231
231
 
232
232
  for (key in options) {
233
- if (this.allowed_find_options.indexOf(key) == -1) {
233
+ if (!this.allowed_find_options.has(key)) {
234
234
  continue;
235
235
  }
236
236
 
@@ -354,90 +354,69 @@ Mongo.setMethod(function _read(context) {
354
354
  // Sorting should happen in the pipeline
355
355
  if (options.sort && options.sort.length) {
356
356
  let sort_object = {};
357
-
357
+
358
358
  for (let entry of options.sort) {
359
- sort_object[entry[0]] = entry[1];
359
+ let field_name = entry[0];
360
+ let assoc_name = entry[2];
361
+
362
+ // If there's an association, prefix the field with the association alias
363
+ if (assoc_name) {
364
+ field_name = assoc_name + '.' + field_name;
365
+ }
366
+
367
+ sort_object[field_name] = entry[1];
360
368
  }
361
-
362
- compiled.pipeline.unshift({$sort: sort_object});
369
+
370
+ // Add $sort at the END of the pipeline (after $lookup stages)
371
+ // so that we can sort by fields from associated models
372
+ compiled.pipeline.push({$sort: sort_object});
363
373
  }
364
-
365
- // Skipping also happens in the pipeline
374
+
375
+ // Use $facet to combine count and data retrieval in a single query
376
+ // This avoids running the expensive $lookup stages twice
377
+ // Note: $skip and $limit go INSIDE the data facet so count reflects total matches
378
+ let data_pipeline = [];
379
+
366
380
  if (options.skip) {
367
- compiled.pipeline.push({$skip: options.skip});
381
+ data_pipeline.push({$skip: options.skip});
368
382
  }
369
-
370
- let aggregate_options = {};
371
-
372
- Function.parallel({
373
- available: function getAvailable(next) {
374
-
375
- if (criteria.options.available === false) {
376
- return next(null, null);
377
- }
378
-
379
- let pipeline = JSON.clone(compiled.pipeline),
380
- cloned_options = JSON.clone(aggregate_options);
381
-
382
- pipeline.push({$count: 'available'});
383
-
384
- // Expensive aggregate just to get the available count...
385
- Pledge.done(collection.aggregate(pipeline, cloned_options), function gotAggregate(err, cursor) {
386
-
387
- if (err) {
388
- return next(err);
389
- }
390
-
391
- Pledge.done(cursor.toArray(), function gotAvailableArray(err, items) {
392
-
393
- if (err) {
394
- return next(err);
395
- }
396
-
397
- if (!items || !items.length) {
398
- return next(null, null);
399
- }
400
-
401
- let available = items[0].available;
402
-
403
- if (options.skip) {
404
- available += options.skip;
405
- }
406
-
407
- return next(null, available);
408
- });
409
- });
410
- },
411
- rows: function getRows(next) {
412
-
413
- let pipeline = JSON.clone(compiled.pipeline);
414
-
415
- // Limits also have to be set in the pipeline now
416
- // (We have to do it here, so the `available` count is correct)
417
- if (options.limit) {
418
- pipeline.push({$limit: options.limit});
419
- }
420
-
421
- Pledge.done(collection.aggregate(pipeline, aggregate_options), function gotAggregate(err, cursor) {
422
-
423
- if (err) {
424
- return next(err);
425
- }
426
-
427
- Pledge.done(cursor.toArray(), next);
428
- });
383
+
384
+ if (options.limit) {
385
+ data_pipeline.push({$limit: options.limit});
386
+ }
387
+
388
+ let facet_stage = {
389
+ $facet: {
390
+ data: data_pipeline,
429
391
  }
430
- }, function done(err, data) {
431
-
432
- if (err) {
433
- return pledge.reject(err);
392
+ };
393
+
394
+ // Only include count facet if available is requested
395
+ if (criteria.options.available !== false) {
396
+ facet_stage.$facet.count = [{$count: 'total'}];
397
+ }
398
+
399
+ compiled.pipeline.push(facet_stage);
400
+
401
+ try {
402
+ let cursor = await collection.aggregate(compiled.pipeline, {});
403
+ let results = await cursor.toArray();
404
+
405
+ let facet_result = results[0] || {};
406
+ let rows = facet_result.data || [];
407
+ let available = null;
408
+
409
+ if (criteria.options.available !== false) {
410
+ available = facet_result.count?.[0]?.total || 0;
434
411
  }
435
-
436
- data.rows = that.organizeResultItems(model, data.rows);
437
-
438
- pledge.resolve(data);
439
- });
440
-
412
+
413
+ rows = that.organizeResultItems(model, rows);
414
+
415
+ pledge.resolve({rows, available});
416
+ } catch (err) {
417
+ pledge.reject(err);
418
+ }
419
+
441
420
  return pledge;
442
421
  }
443
422
 
@@ -457,13 +436,13 @@ Mongo.setMethod(function _read(context) {
457
436
  Pledge.done(cursor.toArray(), next);
458
437
  }
459
438
  }, function done(err, data) {
460
-
439
+
461
440
  if (err) {
462
- return callback(err);
441
+ return pledge.reject(err);
463
442
  }
464
-
443
+
465
444
  data.rows = that.organizeResultItems(model, data.rows);
466
-
445
+
467
446
  pledge.resolve(data);
468
447
  });
469
448
 
@@ -506,38 +485,39 @@ Mongo.setMethod(function _create(context) {
506
485
  data[key] = val;
507
486
  }
508
487
 
509
- Pledge.done(collection.insertOne(data, {w: 1, fullResult: true}), function afterInsert(err, result) {
510
-
511
- // Clear the cache
512
- model.nukeCache();
513
-
514
- if (err != null) {
515
- return pledge.reject(err);
516
- }
488
+ Pledge.done(collection.insertOne(data, {w: 1}), function afterInsert(err, result) {
517
489
 
518
- // @TODO: fix because of mongodb 6
519
- let write_errors = result.message?.documents?.[0]?.writeErrors;
490
+ // Clear the cache
491
+ model.nukeCache();
520
492
 
521
- if (write_errors) {
493
+ if (err != null) {
494
+ // In MongoDB driver 6.x, write errors are thrown as MongoServerError
495
+ // Check if this is a write error that should be converted to validation violations
496
+ if (err.code || err.writeErrors) {
522
497
  let violations = new Classes.Alchemy.Error.Validation.Violations();
523
498
 
524
- if (write_errors.length) {
525
- let entry;
526
-
527
- for (entry of write_errors) {
499
+ // Handle bulk write errors (array of errors)
500
+ if (err.writeErrors && err.writeErrors.length) {
501
+ for (let entry of err.writeErrors) {
528
502
  let violation = new Classes.Alchemy.Error.Validation.Violation();
529
- violation.message = entry.errmsg || entry.message || entry.code;
503
+ violation.message = entry.errmsg || entry.message || String(entry.code);
530
504
  violations.add(violation);
531
505
  }
532
506
  } else {
533
- violations.add(new Error('Unknown database error'));
507
+ // Single error (e.g., duplicate key)
508
+ let violation = new Classes.Alchemy.Error.Validation.Violation();
509
+ violation.message = err.errmsg || err.message || String(err.code);
510
+ violations.add(violation);
534
511
  }
535
512
 
536
513
  return pledge.reject(violations);
537
514
  }
538
515
 
539
- pledge.resolve(Object.assign({}, data));
540
- });
516
+ return pledge.reject(err);
517
+ }
518
+
519
+ pledge.resolve(Object.assign({}, data));
520
+ });
541
521
 
542
522
  return pledge;
543
523
  }
@@ -645,7 +625,7 @@ const performUpdate = (collection, model, context) => {
645
625
  }
646
626
 
647
627
  if (options.debug) {
648
- console.log('Updating with obj', id, update_object);
628
+ log.debug('Updating with obj', id, update_object);
649
629
  }
650
630
 
651
631
  let promise;
@@ -0,0 +1,126 @@
1
+ const DATETIME = Symbol('datetime');
2
+
3
+ /**
4
+ * The custom al-time element
5
+ *
6
+ * @author Jelle De Loecker <jelle@elevenways.be>
7
+ * @since 1.4.0
8
+ * @version 1.4.0
9
+ */
10
+ const AlTime = Function.inherits('Alchemy.Element', 'AlTime');
11
+
12
+ /**
13
+ * The preferred format
14
+ *
15
+ * @author Jelle De Loecker <jelle@elevenways.be>
16
+ * @since 0.3.0
17
+ * @version 0.3.0
18
+ */
19
+ AlTime.setAttribute('format', null, function setFormat(format) {
20
+ this._populate({value: this.datetime, format: format});
21
+ return format;
22
+ });
23
+
24
+ /**
25
+ * Get/set the datetime value
26
+ *
27
+ * @author Jelle De Loecker <jelle@elevenways.be>
28
+ * @since 0.3.0
29
+ * @version 0.3.0
30
+ */
31
+ AlTime.setAttribute('datetime', null, function setDatetime(value) {
32
+ this._populate({value: value});
33
+ return this[DATETIME];
34
+ });
35
+
36
+ /**
37
+ * Set the value with a function call
38
+ *
39
+ * @author Jelle De Loecker <jelle@elevenways.be>
40
+ * @since 0.3.0
41
+ * @version 0.3.0
42
+ */
43
+ AlTime.setMethod(function setDatetime(value) {
44
+ this.datetime = value;
45
+ });
46
+
47
+ /**
48
+ * Populate the element
49
+ *
50
+ * @author Jelle De Loecker <jelle@elevenways.be>
51
+ * @since 0.3.0
52
+ * @version 0.3.0
53
+ *
54
+ * @return {Object[]}
55
+ */
56
+ AlTime.setMethod(function populate() {
57
+ let iso_string = this._populate({value: this.value, format: this.format});
58
+ this[DATETIME] = iso_string;
59
+ return iso_string;
60
+ });
61
+
62
+ /**
63
+ * Populate the element
64
+ *
65
+ * @author Jelle De Loecker <jelle@elevenways.be>
66
+ * @since 0.3.0
67
+ * @version 0.3.0
68
+ */
69
+ AlTime.setMethod(function _populate(input) {
70
+
71
+ if (!input) {
72
+ input = {};
73
+ }
74
+
75
+ let {value, format} = input;
76
+
77
+ if (value == null) {
78
+ value = this[DATETIME];
79
+ }
80
+
81
+ if (format == null) {
82
+ format = this.format;
83
+ }
84
+
85
+ let iso_date,
86
+ date;
87
+
88
+ if (value) {
89
+ date = Date.create(value);
90
+ iso_date = date.toISOString();
91
+ } else {
92
+ iso_date = '';
93
+ }
94
+
95
+ value = iso_date;
96
+
97
+ if (Blast.isServer) {
98
+ return value;
99
+ }
100
+
101
+ let target_element = this.children?.[0];
102
+
103
+ if (!target_element) {
104
+ target_element = this.createElement('time');
105
+ this.append(target_element);
106
+ }
107
+
108
+ let tag_name = target_element.tagName;
109
+ let formatted;
110
+
111
+ if (format) {
112
+ formatted = date.format(format);
113
+ } else {
114
+ formatted = date + '';
115
+ }
116
+
117
+ if (tag_name == 'INPUT') {
118
+ target_element.value = formatted;
119
+ } else {
120
+ if (tag_name == 'TIME') {
121
+ target_element.setAttribute('datetime', iso_date);
122
+ }
123
+
124
+ target_element.textContent = formatted;
125
+ }
126
+ });
@@ -70,17 +70,23 @@ EnumMap.setMethod(function set(name, value) {
70
70
  result = {
71
71
  name : value.name,
72
72
  title : value.title,
73
+ short_title : value.short_title,
73
74
  };
74
75
  } else {
75
76
  result = {
76
77
  name : value.name,
77
78
  title : value.title || value.name,
79
+ short_title : value.short_title,
78
80
  };
79
81
 
80
82
  if (value.icon) {
81
83
  result.icon = value.icon;
82
84
  }
83
85
 
86
+ if (value.icon_style) {
87
+ result.icon_style = value.icon_style;
88
+ }
89
+
84
90
  if (value.color) {
85
91
  result.color = value.color;
86
92
  }
@@ -96,6 +102,9 @@ EnumMap.setMethod(function set(name, value) {
96
102
  }
97
103
 
98
104
  result.number = this.local.size + 1;
105
+
106
+ // "value" does not mean the value that is stored in the database
107
+ // it can be the function that houses info
99
108
  result.value = value;
100
109
  result.is_enumified = true;
101
110