@limetech/lime-crm-building-blocks 1.103.5 → 1.103.7

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 (25) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/limebb-lime-query-builder.cjs.entry.js +2 -4
  3. package/dist/cjs/limebb-lime-query-filter-group_3.cjs.entry.js +123 -12
  4. package/dist/cjs/limebb-lime-query-response-format-editor_2.cjs.entry.js +48 -34
  5. package/dist/collection/components/lime-query-builder/expressions/lime-query-value-input.js +15 -12
  6. package/dist/collection/components/lime-query-builder/lime-query-builder.js +2 -4
  7. package/dist/collection/components/lime-query-builder/response-format/response-format-editor.js +48 -34
  8. package/dist/collection/components/lime-query-builder/type-resolution.js +108 -0
  9. package/dist/components/lime-query-value-input.js +123 -12
  10. package/dist/components/limebb-lime-query-builder.js +2 -4
  11. package/dist/components/response-format-editor.js +48 -34
  12. package/dist/esm/limebb-lime-query-builder.entry.js +2 -4
  13. package/dist/esm/limebb-lime-query-filter-group_3.entry.js +123 -12
  14. package/dist/esm/limebb-lime-query-response-format-editor_2.entry.js +48 -34
  15. package/dist/lime-crm-building-blocks/lime-crm-building-blocks.esm.js +1 -1
  16. package/dist/lime-crm-building-blocks/p-908dd7d5.entry.js +1 -0
  17. package/dist/lime-crm-building-blocks/p-adfb9e90.entry.js +1 -0
  18. package/dist/lime-crm-building-blocks/p-fe6a94a1.entry.js +1 -0
  19. package/dist/types/components/lime-query-builder/expressions/lime-query-value-input.d.ts +1 -2
  20. package/dist/types/components/lime-query-builder/response-format/response-format-editor.d.ts +16 -0
  21. package/dist/types/components/lime-query-builder/type-resolution.d.ts +73 -0
  22. package/package.json +1 -1
  23. package/dist/lime-crm-building-blocks/p-47f4f505.entry.js +0 -1
  24. package/dist/lime-crm-building-blocks/p-4f605428.entry.js +0 -1
  25. package/dist/lime-crm-building-blocks/p-d635e6fc.entry.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [1.103.7](https://github.com/Lundalogik/lime-crm-building-blocks/compare/v1.103.6...v1.103.7) (2025-11-17)
2
+
3
+ ### Bug Fixes
4
+
5
+
6
+ * **lime-query-builder:** fix problem where visual mode would not be in sync with underlying value ([5333966](https://github.com/Lundalogik/lime-crm-building-blocks/commit/5333966b845ad7d980a0280d3e9d52331b37523b)), closes [Lundalogik/crm-insights-and-intelligence#149](https://github.com/Lundalogik/crm-insights-and-intelligence/issues/149)
7
+
8
+ ## [1.103.6](https://github.com/Lundalogik/lime-crm-building-blocks/compare/v1.103.5...v1.103.6) (2025-11-14)
9
+
10
+ ### Bug Fixes
11
+
12
+
13
+ * **lime-query-builder:** render correct input types for date/time properties ([9c8fa6a](https://github.com/Lundalogik/lime-crm-building-blocks/commit/9c8fa6a659ba71dc4b9675f1ce60b4fd7eb30d69)), closes [Lundalogik/crm-insights-and-intelligence#143](https://github.com/Lundalogik/crm-insights-and-intelligence/issues/143)
14
+
1
15
  ## [1.103.5](https://github.com/Lundalogik/lime-crm-building-blocks/compare/v1.103.4...v1.103.5) (2025-11-13)
2
16
 
3
17
  ### Bug Fixes
@@ -32,11 +32,9 @@ const LimeQueryBuilder = class {
32
32
  this.limetype = event.detail;
33
33
  // Reset filter when limetype changes
34
34
  this.filter = undefined;
35
- // Reset response format when limetype changes
35
+ // Reset response format when limetype changes - empty state
36
36
  this.internalResponseFormat = {
37
- object: {
38
- _id: null,
39
- },
37
+ object: {},
40
38
  };
41
39
  this.emitChange();
42
40
  };
@@ -246,6 +246,115 @@ const LimeQueryFilterNotComponent = class {
246
246
  };
247
247
  LimeQueryFilterNotComponent.style = LimebbLimeQueryFilterNotStyle0;
248
248
 
249
+ /**
250
+ * System property name to actual type mapping.
251
+ *
252
+ * Maps system property names (with or without underscore prefix) to their actual data types.
253
+ * This mapping is necessary because system properties have `type='system'` in the metadata
254
+ * (for historical reasons - database schema fieldtype 255), but we need to know their
255
+ * actual data types to render appropriate inputs.
256
+ */
257
+ const SYSTEM_TYPE_MAP = {
258
+ timestamp: 'time',
259
+ createdtime: 'time',
260
+ updatedtime: 'time',
261
+ id: 'integer',
262
+ createduser: 'integer',
263
+ updateduser: 'integer',
264
+ descriptive: 'string',
265
+ };
266
+ /**
267
+ * Lime CRM date/time property type to Lime Elements date picker type mapping.
268
+ *
269
+ * Maps Lime CRM property types to the corresponding Lime Elements date picker types,
270
+ * as they use different naming conventions.
271
+ */
272
+ const DATE_TIME_TYPE_MAP = {
273
+ date: 'date',
274
+ time: 'datetime',
275
+ timeofday: 'time',
276
+ month: 'month',
277
+ quarter: 'quarter',
278
+ year: 'year',
279
+ };
280
+ /**
281
+ * Get the actual type of a property, resolving 'system' type to the underlying type.
282
+ *
283
+ * System properties in Lime CRM have `type: 'system'` in the metadata (a categorical marker
284
+ * from the database schema fieldtype 255), but we need to know their actual data types
285
+ * to render appropriate input controls.
286
+ *
287
+ * @param property - The property to get the type for
288
+ * @param property.type
289
+ * @param property.name
290
+ * @returns The actual property type (resolves 'system' to the underlying type)
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * const property = { name: '_timestamp', type: 'system' };
295
+ * getActualPropertyType(property); // Returns 'time'
296
+ * ```
297
+ */
298
+ function getActualPropertyType(property) {
299
+ if (property.type === 'system') {
300
+ return resolveSystemPropertyType(property.name);
301
+ }
302
+ return property.type;
303
+ }
304
+ /**
305
+ * Map system property names to their actual data types.
306
+ *
307
+ * This mapping is necessary because system properties have `type='system'` in the metadata
308
+ * (for historical reasons - database schema fieldtype 255), but we need to know their
309
+ * actual data types to render appropriate inputs.
310
+ *
311
+ * The mapping is based on the Lime CRM database schema:
312
+ * - System datetime fields: `timestamp`, `createdtime`, `updatedtime`
313
+ * - System integer fields: `id`, `createduser`, `updateduser`
314
+ * - System string fields: `descriptive`
315
+ *
316
+ * @param propertyName - The system property name (with or without underscore prefix)
317
+ * @returns The actual property type
318
+ *
319
+ * @example
320
+ * ```typescript
321
+ * resolveSystemPropertyType('_timestamp'); // Returns 'time'
322
+ * resolveSystemPropertyType('timestamp'); // Returns 'time'
323
+ * resolveSystemPropertyType('_id'); // Returns 'integer'
324
+ * resolveSystemPropertyType('unknown'); // Returns 'string' (default)
325
+ * ```
326
+ */
327
+ function resolveSystemPropertyType(propertyName) {
328
+ const name = propertyName.replace(/^_/, '');
329
+ return SYSTEM_TYPE_MAP[name] || 'string';
330
+ }
331
+ /**
332
+ * Map Lime CRM property types to Lime Elements date picker types.
333
+ *
334
+ * Lime CRM and Lime Elements use different naming conventions for date/time types,
335
+ * so we need to translate between them:
336
+ *
337
+ * - `'date'` → `'date'` - Date only
338
+ * - `'time'` → `'datetime'` - Full datetime (date + time) in Lime CRM
339
+ * - `'timeofday'` → `'time'` - Time only (hh:mm)
340
+ * - `'month'` → `'month'` - Month picker (YYYY-MM)
341
+ * - `'quarter'` → `'quarter'` - Quarter picker
342
+ * - `'year'` → `'year'` - Year picker
343
+ *
344
+ * @param propertyType - The Lime CRM date/time property type
345
+ * @returns The corresponding Lime Elements date picker type
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * mapPropertyTypeToPickerType('time'); // Returns 'datetime'
350
+ * mapPropertyTypeToPickerType('timeofday'); // Returns 'time'
351
+ * mapPropertyTypeToPickerType('date'); // Returns 'date'
352
+ * ```
353
+ */
354
+ function mapPropertyTypeToPickerType(propertyType) {
355
+ return DATE_TIME_TYPE_MAP[propertyType] || 'date';
356
+ }
357
+
249
358
  const limeQueryValueInputCss = ":host{display:flex;gap:0.5rem}:host>*{flex-grow:1}*{box-sizing:border-box}.value-input-container{display:flex;flex-direction:row;align-items:flex-start;gap:0.5rem;width:100%}.mode-toggle{flex-shrink:0;flex-grow:0}.placeholder-input{flex-grow:1;display:flex;flex-direction:column;gap:0.5rem}.placeholder-preview{display:flex;align-items:center;gap:0.5rem;padding:0.5rem;background-color:rgba(var(--color-blue-light), 0.1);border-radius:var(--border-radius-small);font-size:0.875rem;color:rgb(var(--color-blue-default));border-left:3px solid rgb(var(--color-blue-default))}.placeholder-preview limel-icon{flex-shrink:0;color:rgb(var(--color-blue-default))}.placeholder-preview span{font-family:var(--font-monospace);word-break:break-all}";
250
359
  const LimebbLimeQueryValueInputStyle0 = limeQueryValueInputCss;
251
360
 
@@ -408,10 +517,12 @@ const LimeQueryValueInput = class {
408
517
  if (!property) {
409
518
  return this.renderTextInput();
410
519
  }
411
- switch (property.type) {
520
+ // Resolve 'system' type to actual type
521
+ const actualType = getActualPropertyType(property);
522
+ switch (actualType) {
412
523
  case 'integer':
413
524
  case 'decimal': {
414
- return this.renderNumberInput(property.type);
525
+ return this.renderNumberInput(actualType);
415
526
  }
416
527
  case 'yesno': {
417
528
  return this.renderBooleanInput();
@@ -419,11 +530,13 @@ const LimeQueryValueInput = class {
419
530
  case 'option': {
420
531
  return this.renderOptionInput(property);
421
532
  }
422
- case 'date': {
423
- return this.renderDateInput();
424
- }
425
- case 'time': {
426
- return this.renderTimeInput();
533
+ case 'date':
534
+ case 'time':
535
+ case 'timeofday':
536
+ case 'year':
537
+ case 'month':
538
+ case 'quarter': {
539
+ return this.renderDateTimeInput(actualType);
427
540
  }
428
541
  default: {
429
542
  return this.renderTextInput();
@@ -459,13 +572,11 @@ const LimeQueryValueInput = class {
459
572
  const selectedOption = options.find((o) => o.value === this.value);
460
573
  return (index.h("limel-select", { label: this.label, options: options, value: selectedOption, onChange: this.handleSelectChange }));
461
574
  }
462
- renderDateInput() {
575
+ renderDateTimeInput(type) {
463
576
  // Convert string to Date if needed
464
577
  const dateValue = typeof this.value === 'string' ? new Date(this.value) : this.value;
465
- return (index.h("limel-date-picker", { label: this.label, value: dateValue, onChange: this.handleDateChange }));
466
- }
467
- renderTimeInput() {
468
- return (index.h("limel-input-field", { label: this.label, type: "time", value: this.value || '', onChange: this.handleTextChange }));
578
+ const pickerType = mapPropertyTypeToPickerType(type);
579
+ return (index.h("limel-date-picker", { label: this.label, value: dateValue, type: pickerType, onChange: this.handleDateChange }));
469
580
  }
470
581
  renderMultiValueInput() {
471
582
  // For IN operator, allow comma-separated values
@@ -196,46 +196,29 @@ const ResponseFormatEditor = class {
196
196
  };
197
197
  }
198
198
  componentWillLoad() {
199
- var _a;
200
- // Check if value is truly empty ({}) - no object or aggregates
201
- const isTrulyEmpty = this.value &&
202
- Object.keys(this.value).length === 0 &&
203
- !this.value.object &&
204
- !this.value.aggregates;
205
- if (isTrulyEmpty) {
206
- // Keep items empty for truly empty objects
199
+ if (!this.value) {
200
+ // No value provided at all, use default _id
201
+ this.items = [{ path: '_id' }];
202
+ return;
203
+ }
204
+ if (this.isEmptyResponseFormat(this.value)) {
205
+ // Show empty state for explicit empty objects
207
206
  this.items = [];
208
207
  }
209
- else if ((_a = this.value) === null || _a === void 0 ? void 0 : _a.object) {
210
- const converted = propertySelectionToItems(this.value.object);
211
- if (converted.length > 0) {
212
- this.items = converted;
213
- }
214
- else {
215
- // Empty object property, but not a truly empty value
216
- // Use default _id for backward compatibility
217
- this.items = [{ path: '_id' }];
218
- }
208
+ else if (this.value.object) {
209
+ // Has object property with actual properties
210
+ this.items = propertySelectionToItems(this.value.object);
219
211
  }
220
- else if (!this.value) {
221
- // No value provided at all, use default _id
222
- this.items = [{ path: '_id' }];
212
+ else {
213
+ // If value has aggregates but no object property, initialize items to empty array
214
+ this.items = [];
223
215
  }
224
216
  }
225
217
  componentWillUpdate() {
226
- var _a;
227
- // Check if value is truly empty ({})
228
- const isTrulyEmpty = this.value &&
229
- Object.keys(this.value).length === 0 &&
230
- !this.value.object &&
231
- !this.value.aggregates;
232
- if (isTrulyEmpty) {
233
- // Keep items empty for truly empty objects
234
- if (this.items.length > 0) {
235
- this.items = [];
236
- }
218
+ if (!this.value) {
219
+ return;
237
220
  }
238
- else if ((_a = this.value) === null || _a === void 0 ? void 0 : _a.object) {
221
+ if (this.value.object && !this.isEmptyResponseFormat(this.value)) {
239
222
  const currentItems = propertySelectionToItems(this.value.object);
240
223
  // Check if items have changed
241
224
  const itemsChanged = currentItems.length !== this.items.length ||
@@ -247,10 +230,15 @@ const ResponseFormatEditor = class {
247
230
  item.description === current.description);
248
231
  });
249
232
  if (itemsChanged) {
250
- // Allow empty items array - don't force _id
251
233
  this.items = currentItems;
252
234
  }
253
235
  }
236
+ else {
237
+ // Either empty response format or no object property - clear items
238
+ if (this.items.length > 0) {
239
+ this.items = [];
240
+ }
241
+ }
254
242
  }
255
243
  render() {
256
244
  if (!this.limetype) {
@@ -281,6 +269,32 @@ const ResponseFormatEditor = class {
281
269
  };
282
270
  this.change.emit(responseFormat);
283
271
  }
272
+ /**
273
+ * Check if the response format is empty
274
+ *
275
+ * A response format is considered empty in the following cases:
276
+ * - Empty object: `{}`
277
+ * - Object with empty object property and no aggregates: `{ object: {} }`
278
+ *
279
+ * Returns false for:
280
+ * - `null` or `undefined`
281
+ * - Objects with properties: `{ object: { _id: null } }`
282
+ * - Objects with aggregates: `{ aggregates: {...} }`
283
+ *
284
+ * @param value - The ResponseFormat to check
285
+ * @returns True if the response format is empty, false otherwise
286
+ */
287
+ isEmptyResponseFormat(value) {
288
+ if (!value) {
289
+ return false;
290
+ }
291
+ if (Object.keys(value).length === 0) {
292
+ return true;
293
+ }
294
+ return !!(value.object &&
295
+ Object.keys(value.object).length === 0 &&
296
+ !value.aggregates);
297
+ }
284
298
  };
285
299
  ResponseFormatEditor.style = LimebbLimeQueryResponseFormatEditorStyle0;
286
300
 
@@ -11,6 +11,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
11
11
  import { h, Host, } from "@stencil/core";
12
12
  import { Operator, SelectLimeTypes as Limetypes, } from "@limetech/lime-web-components";
13
13
  import { getPropertyFromPath } from "../property-resolution";
14
+ import { getActualPropertyType, mapPropertyTypeToPickerType, } from "../type-resolution";
14
15
  /**
15
16
  * Query Value Input Component
16
17
  *
@@ -182,10 +183,12 @@ export class LimeQueryValueInput {
182
183
  if (!property) {
183
184
  return this.renderTextInput();
184
185
  }
185
- switch (property.type) {
186
+ // Resolve 'system' type to actual type
187
+ const actualType = getActualPropertyType(property);
188
+ switch (actualType) {
186
189
  case 'integer':
187
190
  case 'decimal': {
188
- return this.renderNumberInput(property.type);
191
+ return this.renderNumberInput(actualType);
189
192
  }
190
193
  case 'yesno': {
191
194
  return this.renderBooleanInput();
@@ -193,11 +196,13 @@ export class LimeQueryValueInput {
193
196
  case 'option': {
194
197
  return this.renderOptionInput(property);
195
198
  }
196
- case 'date': {
197
- return this.renderDateInput();
198
- }
199
- case 'time': {
200
- return this.renderTimeInput();
199
+ case 'date':
200
+ case 'time':
201
+ case 'timeofday':
202
+ case 'year':
203
+ case 'month':
204
+ case 'quarter': {
205
+ return this.renderDateTimeInput(actualType);
201
206
  }
202
207
  default: {
203
208
  return this.renderTextInput();
@@ -233,13 +238,11 @@ export class LimeQueryValueInput {
233
238
  const selectedOption = options.find((o) => o.value === this.value);
234
239
  return (h("limel-select", { label: this.label, options: options, value: selectedOption, onChange: this.handleSelectChange }));
235
240
  }
236
- renderDateInput() {
241
+ renderDateTimeInput(type) {
237
242
  // Convert string to Date if needed
238
243
  const dateValue = typeof this.value === 'string' ? new Date(this.value) : this.value;
239
- return (h("limel-date-picker", { label: this.label, value: dateValue, onChange: this.handleDateChange }));
240
- }
241
- renderTimeInput() {
242
- return (h("limel-input-field", { label: this.label, type: "time", value: this.value || '', onChange: this.handleTextChange }));
244
+ const pickerType = mapPropertyTypeToPickerType(type);
245
+ return (h("limel-date-picker", { label: this.label, value: dateValue, type: pickerType, onChange: this.handleDateChange }));
243
246
  }
244
247
  renderMultiValueInput() {
245
248
  // For IN operator, allow comma-separated values
@@ -50,11 +50,9 @@ export class LimeQueryBuilder {
50
50
  this.limetype = event.detail;
51
51
  // Reset filter when limetype changes
52
52
  this.filter = undefined;
53
- // Reset response format when limetype changes
53
+ // Reset response format when limetype changes - empty state
54
54
  this.internalResponseFormat = {
55
- object: {
56
- _id: null,
57
- },
55
+ object: {},
58
56
  };
59
57
  this.emitChange();
60
58
  };
@@ -47,46 +47,29 @@ export class ResponseFormatEditor {
47
47
  };
48
48
  }
49
49
  componentWillLoad() {
50
- var _a;
51
- // Check if value is truly empty ({}) - no object or aggregates
52
- const isTrulyEmpty = this.value &&
53
- Object.keys(this.value).length === 0 &&
54
- !this.value.object &&
55
- !this.value.aggregates;
56
- if (isTrulyEmpty) {
57
- // Keep items empty for truly empty objects
50
+ if (!this.value) {
51
+ // No value provided at all, use default _id
52
+ this.items = [{ path: '_id' }];
53
+ return;
54
+ }
55
+ if (this.isEmptyResponseFormat(this.value)) {
56
+ // Show empty state for explicit empty objects
58
57
  this.items = [];
59
58
  }
60
- else if ((_a = this.value) === null || _a === void 0 ? void 0 : _a.object) {
61
- const converted = propertySelectionToItems(this.value.object);
62
- if (converted.length > 0) {
63
- this.items = converted;
64
- }
65
- else {
66
- // Empty object property, but not a truly empty value
67
- // Use default _id for backward compatibility
68
- this.items = [{ path: '_id' }];
69
- }
59
+ else if (this.value.object) {
60
+ // Has object property with actual properties
61
+ this.items = propertySelectionToItems(this.value.object);
70
62
  }
71
- else if (!this.value) {
72
- // No value provided at all, use default _id
73
- this.items = [{ path: '_id' }];
63
+ else {
64
+ // If value has aggregates but no object property, initialize items to empty array
65
+ this.items = [];
74
66
  }
75
67
  }
76
68
  componentWillUpdate() {
77
- var _a;
78
- // Check if value is truly empty ({})
79
- const isTrulyEmpty = this.value &&
80
- Object.keys(this.value).length === 0 &&
81
- !this.value.object &&
82
- !this.value.aggregates;
83
- if (isTrulyEmpty) {
84
- // Keep items empty for truly empty objects
85
- if (this.items.length > 0) {
86
- this.items = [];
87
- }
69
+ if (!this.value) {
70
+ return;
88
71
  }
89
- else if ((_a = this.value) === null || _a === void 0 ? void 0 : _a.object) {
72
+ if (this.value.object && !this.isEmptyResponseFormat(this.value)) {
90
73
  const currentItems = propertySelectionToItems(this.value.object);
91
74
  // Check if items have changed
92
75
  const itemsChanged = currentItems.length !== this.items.length ||
@@ -98,10 +81,15 @@ export class ResponseFormatEditor {
98
81
  item.description === current.description);
99
82
  });
100
83
  if (itemsChanged) {
101
- // Allow empty items array - don't force _id
102
84
  this.items = currentItems;
103
85
  }
104
86
  }
87
+ else {
88
+ // Either empty response format or no object property - clear items
89
+ if (this.items.length > 0) {
90
+ this.items = [];
91
+ }
92
+ }
105
93
  }
106
94
  render() {
107
95
  if (!this.limetype) {
@@ -132,6 +120,32 @@ export class ResponseFormatEditor {
132
120
  };
133
121
  this.change.emit(responseFormat);
134
122
  }
123
+ /**
124
+ * Check if the response format is empty
125
+ *
126
+ * A response format is considered empty in the following cases:
127
+ * - Empty object: `{}`
128
+ * - Object with empty object property and no aggregates: `{ object: {} }`
129
+ *
130
+ * Returns false for:
131
+ * - `null` or `undefined`
132
+ * - Objects with properties: `{ object: { _id: null } }`
133
+ * - Objects with aggregates: `{ aggregates: {...} }`
134
+ *
135
+ * @param value - The ResponseFormat to check
136
+ * @returns True if the response format is empty, false otherwise
137
+ */
138
+ isEmptyResponseFormat(value) {
139
+ if (!value) {
140
+ return false;
141
+ }
142
+ if (Object.keys(value).length === 0) {
143
+ return true;
144
+ }
145
+ return !!(value.object &&
146
+ Object.keys(value.object).length === 0 &&
147
+ !value.aggregates);
148
+ }
135
149
  static get is() { return "limebb-lime-query-response-format-editor"; }
136
150
  static get encapsulation() { return "shadow"; }
137
151
  static get originalStyleUrls() {
@@ -0,0 +1,108 @@
1
+ /**
2
+ * System property name to actual type mapping.
3
+ *
4
+ * Maps system property names (with or without underscore prefix) to their actual data types.
5
+ * This mapping is necessary because system properties have `type='system'` in the metadata
6
+ * (for historical reasons - database schema fieldtype 255), but we need to know their
7
+ * actual data types to render appropriate inputs.
8
+ */
9
+ const SYSTEM_TYPE_MAP = {
10
+ timestamp: 'time',
11
+ createdtime: 'time',
12
+ updatedtime: 'time',
13
+ id: 'integer',
14
+ createduser: 'integer',
15
+ updateduser: 'integer',
16
+ descriptive: 'string',
17
+ };
18
+ /**
19
+ * Lime CRM date/time property type to Lime Elements date picker type mapping.
20
+ *
21
+ * Maps Lime CRM property types to the corresponding Lime Elements date picker types,
22
+ * as they use different naming conventions.
23
+ */
24
+ const DATE_TIME_TYPE_MAP = {
25
+ date: 'date',
26
+ time: 'datetime',
27
+ timeofday: 'time',
28
+ month: 'month',
29
+ quarter: 'quarter',
30
+ year: 'year',
31
+ };
32
+ /**
33
+ * Get the actual type of a property, resolving 'system' type to the underlying type.
34
+ *
35
+ * System properties in Lime CRM have `type: 'system'` in the metadata (a categorical marker
36
+ * from the database schema fieldtype 255), but we need to know their actual data types
37
+ * to render appropriate input controls.
38
+ *
39
+ * @param property - The property to get the type for
40
+ * @param property.type
41
+ * @param property.name
42
+ * @returns The actual property type (resolves 'system' to the underlying type)
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const property = { name: '_timestamp', type: 'system' };
47
+ * getActualPropertyType(property); // Returns 'time'
48
+ * ```
49
+ */
50
+ export function getActualPropertyType(property) {
51
+ if (property.type === 'system') {
52
+ return resolveSystemPropertyType(property.name);
53
+ }
54
+ return property.type;
55
+ }
56
+ /**
57
+ * Map system property names to their actual data types.
58
+ *
59
+ * This mapping is necessary because system properties have `type='system'` in the metadata
60
+ * (for historical reasons - database schema fieldtype 255), but we need to know their
61
+ * actual data types to render appropriate inputs.
62
+ *
63
+ * The mapping is based on the Lime CRM database schema:
64
+ * - System datetime fields: `timestamp`, `createdtime`, `updatedtime`
65
+ * - System integer fields: `id`, `createduser`, `updateduser`
66
+ * - System string fields: `descriptive`
67
+ *
68
+ * @param propertyName - The system property name (with or without underscore prefix)
69
+ * @returns The actual property type
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * resolveSystemPropertyType('_timestamp'); // Returns 'time'
74
+ * resolveSystemPropertyType('timestamp'); // Returns 'time'
75
+ * resolveSystemPropertyType('_id'); // Returns 'integer'
76
+ * resolveSystemPropertyType('unknown'); // Returns 'string' (default)
77
+ * ```
78
+ */
79
+ export function resolveSystemPropertyType(propertyName) {
80
+ const name = propertyName.replace(/^_/, '');
81
+ return SYSTEM_TYPE_MAP[name] || 'string';
82
+ }
83
+ /**
84
+ * Map Lime CRM property types to Lime Elements date picker types.
85
+ *
86
+ * Lime CRM and Lime Elements use different naming conventions for date/time types,
87
+ * so we need to translate between them:
88
+ *
89
+ * - `'date'` → `'date'` - Date only
90
+ * - `'time'` → `'datetime'` - Full datetime (date + time) in Lime CRM
91
+ * - `'timeofday'` → `'time'` - Time only (hh:mm)
92
+ * - `'month'` → `'month'` - Month picker (YYYY-MM)
93
+ * - `'quarter'` → `'quarter'` - Quarter picker
94
+ * - `'year'` → `'year'` - Year picker
95
+ *
96
+ * @param propertyType - The Lime CRM date/time property type
97
+ * @returns The corresponding Lime Elements date picker type
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * mapPropertyTypeToPickerType('time'); // Returns 'datetime'
102
+ * mapPropertyTypeToPickerType('timeofday'); // Returns 'time'
103
+ * mapPropertyTypeToPickerType('date'); // Returns 'date'
104
+ * ```
105
+ */
106
+ export function mapPropertyTypeToPickerType(propertyType) {
107
+ return DATE_TIME_TYPE_MAP[propertyType] || 'date';
108
+ }