data-primals-engine 1.4.2 → 1.5.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 (59) hide show
  1. package/README.md +878 -856
  2. package/client/package-lock.json +82 -0
  3. package/client/package.json +2 -0
  4. package/client/src/App.jsx +1 -1
  5. package/client/src/App.scss +25 -7
  6. package/client/src/AssistantChat.scss +3 -2
  7. package/client/src/ConditionBuilder.jsx +1 -1
  8. package/client/src/ConditionBuilder2.jsx +2 -1
  9. package/client/src/DashboardView.jsx +569 -569
  10. package/client/src/DataEditor.jsx +376 -368
  11. package/client/src/DataLayout.jsx +4 -10
  12. package/client/src/DataTable.jsx +858 -817
  13. package/client/src/Field.jsx +1825 -1784
  14. package/client/src/FlexDataRenderer.jsx +2 -0
  15. package/client/src/FlexTreeUtils.js +1 -1
  16. package/client/src/GeolocationField.jsx +94 -0
  17. package/client/src/KPIDialog.jsx +11 -1
  18. package/client/src/ModelCreator.jsx +1 -2
  19. package/client/src/ModelCreatorField.jsx +24 -27
  20. package/client/src/ModelList.jsx +1 -1
  21. package/client/src/RelationField.jsx +2 -2
  22. package/client/src/constants.js +3 -3
  23. package/client/src/filter.js +0 -155
  24. package/client/src/hooks/useTutorials.jsx +62 -65
  25. package/client/src/translations.js +14 -2
  26. package/package.json +2 -1
  27. package/perf/README.md +147 -0
  28. package/perf/artillery-hooks.js +37 -0
  29. package/perf/perf-shot-hardwork.yml +84 -0
  30. package/perf/perf-shot-search.yml +45 -0
  31. package/perf/setup.yml +26 -0
  32. package/server.js +1 -1
  33. package/src/constants.js +264 -31
  34. package/src/core.js +15 -1
  35. package/src/data.js +1 -1
  36. package/src/defaultModels.js +1544 -1540
  37. package/src/email.js +5 -2
  38. package/src/engine.js +10 -3
  39. package/src/filter.js +274 -260
  40. package/src/i18n.js +187 -177
  41. package/src/modules/assistant/assistant.js +3 -1
  42. package/src/modules/bucket.js +12 -15
  43. package/src/modules/data/data.backup.js +11 -8
  44. package/src/modules/data/data.core.js +2 -1
  45. package/src/modules/data/data.js +6 -3
  46. package/src/modules/data/data.operations.js +610 -168
  47. package/src/modules/data/data.routes.js +1821 -1785
  48. package/src/modules/data/data.scheduling.js +2 -1
  49. package/src/modules/data/data.validation.js +7 -1
  50. package/src/modules/file.js +4 -2
  51. package/src/modules/user.js +4 -1
  52. package/src/modules/workflow.js +9 -10
  53. package/src/openai.jobs.js +2 -0
  54. package/src/packs.js +22 -5
  55. package/src/providers.js +22 -7
  56. package/swagger-en.yml +133 -0
  57. package/test/data.integration.test.js +1060 -981
  58. package/test/import_export.integration.test.js +1 -1
  59. package/test/model.integration.test.js +377 -221
@@ -33,8 +33,10 @@ const formatDate = (value, type, t) => {
33
33
 
34
34
  const FlexDataRenderer = ({ value, fieldDefinition,data }) => {
35
35
  const { t, i18n } = useTranslation();
36
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
36
37
  const {models} = useModelContext()
37
38
  const {me} = useAuthContext()
39
+
38
40
  // 'translatedLabel' is added by getFieldDefinitionByPath
39
41
  const field = fieldDefinition;
40
42
  const translatedLabel = t('model_'+field.name, field.name);
@@ -36,7 +36,7 @@ export const createNewNode = (type) => {
36
36
  return {
37
37
  ...baseNode,
38
38
  type: 'item',
39
- itemStyle: { width: '100px', height: '50px' },
39
+ itemStyle: { },
40
40
  content: { type: 'placeholder' }
41
41
  };
42
42
  }
@@ -0,0 +1,94 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { MapContainer, TileLayer, Marker, useMapEvents, Popup } from 'react-leaflet';
3
+ import 'leaflet/dist/leaflet.css';
4
+ import L from 'leaflet';
5
+
6
+ // Fix for default marker icon issue with webpack/parcel
7
+ delete L.Icon.Default.prototype._getIconUrl;
8
+
9
+ L.Icon.Default.mergeOptions({
10
+ iconRetinaUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png',
11
+ iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
12
+ shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png'
13
+ });
14
+
15
+
16
+ const LocationMarker = ({ position, setPosition, name, onChange, value }) => {
17
+ const map = useMapEvents({
18
+ click(e) {
19
+ const { lat, lng } = e.latlng;
20
+ const newPos = [lat, lng];
21
+ setPosition(newPos);
22
+ map.flyTo(newPos, map.getZoom());
23
+ onChange({
24
+ name: name,
25
+ value: {
26
+ type: 'Point',
27
+ coordinates: [lng, lat]
28
+ }
29
+ });
30
+ },
31
+ locationfound(e) {
32
+ const { lat, lng } = e.latlng;
33
+ const newPos = [lat, lng];
34
+ setPosition(newPos);
35
+ map.flyTo(newPos, map.getZoom());
36
+ onChange({
37
+ name: name,
38
+ value: {
39
+ type: 'Point',
40
+ coordinates: [lng, lat]
41
+ }
42
+ });
43
+ },
44
+ });
45
+
46
+ useEffect(() => {
47
+ // If there's no value, try to locate user
48
+ if (!value) {
49
+ map.locate();
50
+ }
51
+ }, [map, value]);
52
+
53
+
54
+ return position === null ? null : (
55
+ <Marker position={position}>
56
+ <Popup>Selected location</Popup>
57
+ </Marker>
58
+ );
59
+ }
60
+
61
+ const GeolocationField = ({ value, onChange, name }) => {
62
+ // Default to a central location if no value is provided, e.g., Paris
63
+ const initialPosition = (value && value.coordinates && value.coordinates.length === 2)
64
+ ? [value.coordinates[1], value.coordinates[0]] // Leaflet uses [lat, lng], GeoJSON uses [lng, lat]
65
+ : [48.8566, 2.3522]; // Default to Paris
66
+
67
+ const [position, setPosition] = useState(initialPosition);
68
+
69
+ useEffect(() => {
70
+ if (value && value.coordinates && value.coordinates.length === 2) {
71
+ const newPos = [value.coordinates[1], value.coordinates[0]];
72
+ if (position[0] !== newPos[0] || position[1] !== newPos[1]) {
73
+ setPosition(newPos);
74
+ }
75
+ } else {
76
+ // If value is cleared, reset to default. The map.locate() will try to find the user.
77
+ setPosition([48.8566, 2.3522]);
78
+ }
79
+ }, [value]);
80
+
81
+ return (
82
+ <div style={{ height: '300px', width: '100%' }}>
83
+ <MapContainer center={position} zoom={13} scrollWheelZoom={true} style={{ height: '100%', width: '100%' }}>
84
+ <TileLayer
85
+ attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
86
+ url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
87
+ />
88
+ <LocationMarker position={position} setPosition={setPosition} name={name} onChange={onChange} value={value} />
89
+ </MapContainer>
90
+ </div>
91
+ );
92
+ };
93
+
94
+ export default GeolocationField;
@@ -1,11 +1,16 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Dialog } from './Dialog.jsx'; // Assure-toi que Dialog est importé
3
3
  import { useTranslation } from 'react-i18next';
4
+ import {FaAd, FaPlus} from "react-icons/fa";
5
+ import {getUserHash} from "../../src/data.js";
6
+ import {useNavigate} from "react-router-dom";
7
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
4
8
 
5
9
  function KPIDialog({ availableKpis = [], onAddKpi, onClose }) {
6
10
  const { t } = useTranslation();
7
11
  const [searchTerm, setSearchTerm] = useState('');
8
-
12
+ const nav = useNavigate()
13
+ const {me} = useAuthContext()
9
14
  const filteredKpis = (availableKpis || []).filter(kpi =>
10
15
  kpi.name.value.toLowerCase().includes(searchTerm.toLowerCase())
11
16
  );
@@ -32,6 +37,11 @@ function KPIDialog({ availableKpis = [], onAddKpi, onClose }) {
32
37
  <li>{t('dashboards.no_kpi_found', 'Aucun KPI disponible ou correspondant.')}</li>
33
38
  )}
34
39
  </ul>
40
+ <div className={"flex mg-2"}>
41
+ <button onClick={() => {
42
+ nav(`/user/${getUserHash(me)}/?model=kpi`);
43
+ }} className={"btn"}><FaPlus />{t('dashboards.addKpi', 'Nouvel indicateur KPI')}</button>
44
+ </div>
35
45
  </Dialog>
36
46
  );
37
47
  }
@@ -660,11 +660,10 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
660
660
  {/* Boutons d'action, visibles si un modèle est affiché ou en mode manuel */}
661
661
  {(showModel || !useAI || initialModel) && (
662
662
  <div className="actions flex">
663
- {initialModel && (
663
+
664
664
  <Button type="button" onClick={handleAddField}>
665
665
  <FaPlus /> <Trans i18nKey={"btns.addField"}>Ajouter un champ</Trans>
666
666
  </Button>
667
- )}
668
667
 
669
668
  <Button type="submit">
670
669
  <FaSave />
@@ -72,7 +72,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
72
72
  newFields[index].name = e.target.value;
73
73
  setFields(newFields);
74
74
  }}
75
- help={t('modelcreator.name.hint')}
76
75
  required
77
76
  after={!(!modelLocked && isLocalUser(me) && field.locked) && !field._isNewField && (
78
77
  <Button type={"button"} className={"btn-form btn-last"}
@@ -117,6 +116,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
117
116
  {label: t("field.array"), value: "array"},
118
117
  {label: t("field.relation"), value: "relation"},
119
118
  {label: t("field.file"), value: "file"},
119
+ {label: t("field.geolocation"), value: "geolocation"},
120
120
  {label: t("field.code"), value: "code"},
121
121
  {label: t("field.calculated", "calculated data"), value: "calculated"},
122
122
  {label: t("field.cronSchedule", "task schedule"), value: "cronSchedule"},
@@ -211,10 +211,10 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
211
211
 
212
212
  <div className="flex flex-no-wrap">
213
213
  {hint('modelcreator.relationFilter.hint')}
214
- <label className="checkbox-label flex flex-1"><Trans
215
- i18nKey={"modelcreator.relationFilter"}>Filtre</Trans> :
216
- <input
217
- type="checkbox"
214
+ <div className="checkbox-label flex flex-1">
215
+ <CheckboxField
216
+ label={<Trans
217
+ i18nKey={"modelcreator.relationFilter"}>Filtre</Trans>}
218
218
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
219
219
  checked={field.relationFilter !== undefined}
220
220
  onChange={(e) => {
@@ -227,7 +227,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
227
227
  setFields(newFields);
228
228
  }}
229
229
  />
230
- </label>
230
+ </div>
231
231
  </div>)}
232
232
 
233
233
 
@@ -285,7 +285,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
285
285
  newFields[index].multiline = e;
286
286
  setFields(newFields);
287
287
  }}
288
- help={field.multiline && t('modelcreator.multiline.hint')}
289
288
  />
290
289
  </div>
291
290
  </div>
@@ -294,28 +293,26 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
294
293
 
295
294
  {['string', 'string_t', 'richtext', 'richtext_t', 'email', 'phone', 'url', 'password', 'code'].includes(field.itemsType || field.type) && (
296
295
  <>
297
- <div className={"flex flex-no-wrap"}>
296
+ <div className={"flex flex-no-wrap field-bg"}>
298
297
  {hint('modelcreator.maxlength.hint')}
299
- <label className={"flex flex-1"}>
300
- <Trans i18nKey={"modelcreator.maxlength"}>Longueur
301
- maximale :</Trans>
302
- <NumberField
298
+ <div className={"flex-1"}><NumberField
303
299
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
304
300
  step={1}
305
301
  min={0}
302
+ label={<Trans i18nKey={"modelcreator.maxlength"}>Longueur
303
+ maximale :</Trans>}
306
304
  className={"flex-1"}
307
305
  value={field.maxlength}
308
306
  onChange={(e) => {
309
307
  const newFields = [...fields];
310
- const val = parseInt(e, 10);
311
- if (!val)
308
+ const val = parseInt(e.target.value, 10);
309
+ if (isNaN(val))
312
310
  newFields[index].maxlength = undefined;
313
311
  else
314
312
  newFields[index].maxlength = val;
315
313
  setFields(newFields);
316
314
  }}
317
- />
318
- </label>
315
+ /></div>
319
316
  </div>
320
317
  </>
321
318
  )}
@@ -437,7 +434,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
437
434
  newFields[index].required = e;
438
435
  setFields(newFields);
439
436
  }}
440
- help={field.required && t('modelcreator.required.hint')}
441
437
  />
442
438
  </div>
443
439
  </div>
@@ -455,7 +451,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
455
451
  newFields[index].unique = e;
456
452
  setFields(newFields);
457
453
  }}
458
- help={field.unique && t('modelcreator.unique.hint')}
459
454
  />
460
455
  </div>
461
456
  </div>
@@ -554,7 +549,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
554
549
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
555
550
  onChange={(e) => {
556
551
  const newFields = [...fields];
557
-
558
552
  newFields[index].default = e.value;
559
553
  setFields(newFields);
560
554
  }}
@@ -625,10 +619,12 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
625
619
 
626
620
  {(['datetime', 'date'].includes(field.itemsType || field.type)) && (
627
621
  <>
628
- <div className="flex flex-no-wrap mg-item">
622
+ <div
623
+ className="flex flex-no-wrap mg-item">
629
624
  {hint('modelcreator.min.hint')}
630
- <label className={"flex-1"}><Trans i18nKey={"modelcreator.min"}>Valeur minimale :</Trans></label>
631
- <div className="flex flex-1 field-bg "><input
625
+ <div className="flex field-bg flex-1">
626
+ <label className={"flex-1"}><Trans i18nKey={"modelcreator.min"}>Valeur minimale :</Trans></label>
627
+ <input
632
628
  className={"flex-1"}
633
629
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
634
630
  type={field.type === 'datetime' ? 'datetime-local' : field.type}
@@ -642,13 +638,16 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
642
638
  }
643
639
  setFields(newFields);
644
640
  }}
645
- /></div>
641
+ />
642
+ </div>
646
643
  </div>
647
644
 
648
645
  <div className="flex flex-no-wrap mg-item">
649
646
  {hint('modelcreator.max.hint')}
650
- <label className={"flex-1"}><Trans i18nKey={"modelcreator.max"}>Valeur maximale :</Trans></label>
651
- <div className="flex flex-1 field-bg "><input
647
+ <div className="flex field-bg flex-1">
648
+ <label className={"flex-1"}><Trans i18nKey={"modelcreator.max"}>Valeur maximale :</Trans></label>
649
+ <input
650
+ className={"flex-1"}
652
651
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
653
652
  type={field.type === 'datetime' ? 'datetime-local' : field.type}
654
653
  value={field.type === "number" ? (field.max + '').replace('.', ',') : field.max}
@@ -806,7 +805,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
806
805
  newFields[index].asMain = e;
807
806
  setFields(newFields);
808
807
  }}
809
- help={field.asMain && t('modelcreator.asMain.hint')}
810
808
  />
811
809
  </div>
812
810
  </div>)}
@@ -857,7 +855,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
857
855
  newFields[index].hiddenable = e;
858
856
  setFields(newFields);
859
857
  }}
860
- help={field.unique && t('modelcreator.hiddenable.hint')}
861
858
  />
862
859
  </div>
863
860
  </div>
@@ -190,7 +190,7 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
190
190
  key={'modelist' + model.name} onClick={() => generatedModels.some(g => g.name === model.name) ? onEditModel(model) : onModelSelect(model)}>
191
191
  <div className="flex flex-center flex-fw">
192
192
  <div
193
- className="flex flex-1 flex-no-wrap break-word">
193
+ className="flex flex-1 flex-no-wrap break-word gap-2">
194
194
  <div className={"icon"}>{IconComponent ? IconComponent : <></>}</div>
195
195
  <div>{t(`model_${model.name}`, model.name)} {generatedModels.some(f => f.name === model.name) ? '(tmp)' : (mods.some(f => f.mod === model.name) ? `(${mods.find(f => f.mod === model.name).count})` : '')}</div></div>
196
196
  <div className="btns">
@@ -43,14 +43,14 @@ const RelationField = ({ field, help, onFocus, onBlur, onChange, value = null })
43
43
  // Recherche sur les champs principaux (asMain)
44
44
  model.fields.forEach(f => {
45
45
  if (f.asMain && mainFieldsTypes.includes(f.type)) {
46
- orConditions.push({ [f.name]: { "$regex": searchValue, "$options": "i" } });
46
+ orConditions.push({"$regexMatch": {input: "$" + f.name, regex: searchValue}});
47
47
  }
48
48
  });
49
49
  // Si aucun champ principal, recherche sur les champs texte
50
50
  if (orConditions.length === 0) {
51
51
  model.fields.forEach(f => {
52
52
  if (["string", "string_t", "richtext", "url"].includes(f.type)) {
53
- orConditions.push({ [f.name]: { "$regex": searchValue, "$options": "i" } });
53
+ orConditions.push({"$regexMatch": {input: "$" + f.name, regex: searchValue}});
54
54
  }
55
55
  });
56
56
  }
@@ -57,14 +57,14 @@ export const profiles = {
57
57
  }, // budget,
58
58
  'developer': {
59
59
  "packs": ["Multilingual starter pack", "Website Starter Pack"],
60
- "models": ['alert','endpoint','request','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role']
60
+ "models": ['alert','endpoint','request','kpi','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role']
61
61
  },
62
62
  'company': {
63
- "packs": ["E-commerce Starter Kit"],
63
+ "packs": ["Multilingual starter pack", "E-commerce Starter Kit"],
64
64
  "models": ['alert','request','location', 'order', 'currency', 'product', 'cart', 'cartItem', 'invoice', 'message', 'user', 'role', 'permission', 'token','translation', 'lang', 'webpage', 'content', 'taxonomy', 'contact', 'resource', 'accountingExercise', 'accountingLineItem', 'accountingEntry', 'employee', 'kpi', 'dashboard']
65
65
  },
66
66
  'engineer': {
67
- "packs": ["AI Content Generation - Starter Pack"],
67
+ "packs": ["Multilingual starter pack", "AI Content Generation - Starter Pack"],
68
68
  "models":['alert','endpoint','request','dashboard', 'kpi', 'user', 'role', 'token', 'permission', 'workflow', 'workflowRun', 'workflowStep', "channel", "message", 'workflowAction', 'workflowTrigger']
69
69
  }
70
70
  }
@@ -1,159 +1,4 @@
1
1
 
2
- export const MONGO_CALC_OPERATORS = {
3
- $find: {
4
- label: 'Find in relation',
5
- description: 'Recherche dans une relation/tableau',
6
- supportsNesting: true,
7
- multi: false,
8
- isFindOperator: true // Nouvelle propriété pour identifier les opérateurs $find
9
- },
10
- '$regexMatch': {
11
- label: 'Find by regex',
12
- description: 'Find a string matching a regular expression (Ecmascript)',
13
- args: 2, // input et regex
14
- specialStructure: true // Indique une structure spéciale
15
- },
16
- $and: { label: 'Et (and)', description: 'Renvoie vrai si toutes les conditions sont vérifiées', args: 1, multi: true },
17
- $or: { label: 'Ou (or)', description: 'Renvoie vrai si au moins une des conditions est vérifiée.', args: 1, multi: true },
18
- $not: { label: 'Non (not)', description: 'Inverse la condition (ex: non égal à).', args: 1, multi: true },
19
- $nor: { label: 'Ou Non (nor)', description: 'Renvoie vrai si aucune des conditions n\'est vérifiée', args: 1, multi: true },
20
- $eq: { label: '=', multi: false, args: 2 },
21
- $ne: { label: '!=', multi: false, args: 2 },
22
- $gt: { label: '>', multi: false, args: 2 },
23
- $gte: { label: '>=', multi: false, args: 2 },
24
- $lt: { label: '<', multi: false, args: 2 },
25
- $lte: { label: '<=', multi: false, args: 2 },
26
- $in: { label: 'in', multi: true, args: 2 },
27
- $nin: { label: 'nin', multi: true, args: 2 },
28
- $all: { label: 'afll', multi: true },
29
- $size: {
30
- label: 'size',
31
- multi: false,
32
- description: 'Renvoie le nombre d\'éléments dans un tableau',
33
- disableAdvancedValue: true
34
- },
35
- $elemMatch: { label: 'elemMatch', multi: true },
36
- $type: {
37
- label: 'type',
38
- description: 'Renvoie le type BSON de l\'élément',
39
- multi: false,
40
- disableAdvancedValue: true
41
- },
42
- $add: { label: '+', multi: true },
43
- $subtract: { label: '-', multi: false, args: 2 },
44
- $multiply: { label: '*', multi: true },
45
- $divide: { label: '/', multi: false, args: 2 },
46
- $mod: { label: '%', multi: false, args: 2 },
47
- $pow: { label: 'pow', multi: false },
48
- $sqrt: { label: 'sqrt', multi: false },
49
- $exp: { label: 'exp', multi: false },
50
- $abs: { label: 'abs', multi: false },
51
- $ceil: { label: 'ceil', multi: false },
52
- $floor: { label: 'floor', multi: false },
53
- $ln: { label: 'ln', multi: false },
54
- $log10: { label: 'log', multi: false },
55
- $concat: { label: 'concat', multi: true },
56
-
57
- // Date-specific
58
- $year: { label: 'year', multi: false, isDate: true },
59
- $month: { label: 'month', multi: false, isDate: true },
60
- $dayOfMonth: { label: 'dayOfMonth', multi: false, isDate: true },
61
- $hour: { label: 'hour', multi: false, isDate: true },
62
- $minute: { label: 'minute', multi: false, isDate: true },
63
- $second: { label: 'second', multi: false, isDate: true },
64
- $millisecond: { label: 'ms', multi: false, isDate: true },
65
- // Date operators
66
- $dateAdd: {
67
- label: 'Add to date',
68
- description: 'Adds a duration to a date',
69
- isDate: true,
70
- specialStructure: true,
71
- args: [
72
- { name: 'startDate', label: 'Start Date', type: 'date' },
73
- { name: 'unit', label: 'Unit', type: 'select', options: ['year', 'month', 'day', 'hour', 'minute', 'second'] },
74
- { name: 'amount', label: 'Amount', type: 'number' },
75
- { name: 'timezone', label: 'Timezone', type: 'text', optional: true }
76
- ]
77
- },
78
- $dateSubtract: {
79
- label: 'Subtract from date',
80
- description: 'Subtracts a duration from a date',
81
- isDate: true,
82
- specialStructure: true,
83
- args: [
84
- { name: 'startDate', label: 'Start Date', type: 'date' },
85
- { name: 'unit', label: 'Unit', type: 'select', options: ['year', 'month', 'day', 'hour', 'minute', 'second'] },
86
- { name: 'amount', label: 'Amount', type: 'number' },
87
- { name: 'timezone', label: 'Timezone', type: 'text', optional: true }
88
- ]
89
- },
90
- $dateDiff: {
91
- label: 'Date Difference',
92
- description: 'Calculates the difference between two dates in specified units',
93
- isDate: true,
94
- specialStructure: true,
95
- args: [
96
- {
97
- name: 'startDate',
98
- label: 'Start Date',
99
- type: 'date',
100
- description: 'The starting date (inclusive)'
101
- },
102
- {
103
- name: 'endDate',
104
- label: 'End Date',
105
- type: 'date',
106
- description: 'The ending date (exclusive)'
107
- },
108
- {
109
- name: 'unit',
110
- label: 'Unit',
111
- type: 'select',
112
- options: ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'],
113
- description: 'The unit for the result'
114
- },
115
- {
116
- name: 'timezone',
117
- label: 'Timezone',
118
- type: 'text',
119
- optional: true,
120
- description: 'The timezone (e.g. "Europe/Paris")'
121
- }
122
- ]
123
- },
124
- $dateToString: {
125
- label: 'Format date as string',
126
- description: 'Formats a date as a string',
127
- isDate: true,
128
- specialStructure: true,
129
- args: [
130
- {
131
- name: 'date',
132
- label: 'Date',
133
- type: 'date'
134
- },
135
- {
136
- name: 'format',
137
- label: 'Format',
138
- optional: true,
139
- type: 'text'
140
- },
141
- {
142
- name: 'timezone',
143
- label: 'Timezone',
144
- type: 'text',
145
- optional: true,
146
- description: 'The timezone (e.g. "Europe/Paris")'
147
- }
148
- ]
149
- },
150
-
151
- // Converters
152
- $toBool: { label: 'toBool', multi: false, converter: true },
153
- $toString: { label: 'toString', multi: false, converter: true },
154
- $toInt: { label: 'toInt', multi: false, converter: true },
155
- $toDouble: { label: 'toDouble', multi: false, converter: true }
156
- };
157
2
 
158
3
  export const convertInputValue = (value) => {
159
4
  if (typeof value !== 'string') return value;