data-primals-engine 1.4.1 → 1.4.3

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 (49) hide show
  1. package/README.md +37 -22
  2. package/client/package-lock.json +33 -0
  3. package/client/package.json +1 -0
  4. package/client/src/App.scss +12 -4
  5. package/client/src/AssistantChat.jsx +48 -5
  6. package/client/src/AssistantChat.scss +0 -1
  7. package/client/src/ChartConfigModal.jsx +34 -9
  8. package/client/src/ConditionBuilder.jsx +1 -1
  9. package/client/src/ConditionBuilder2.jsx +2 -1
  10. package/client/src/Dashboard.jsx +396 -349
  11. package/client/src/DashboardChart.jsx +91 -12
  12. package/client/src/DataEditor.jsx +376 -368
  13. package/client/src/DataLayout.jsx +3 -2
  14. package/client/src/DataTable.jsx +60 -31
  15. package/client/src/Field.jsx +1788 -1782
  16. package/client/src/GeolocationField.jsx +94 -0
  17. package/client/src/ModelCreatorField.jsx +1 -0
  18. package/client/src/RelationField.jsx +2 -2
  19. package/client/src/constants.js +2 -2
  20. package/client/src/contexts/UIContext.jsx +5 -2
  21. package/client/src/filter.js +0 -155
  22. package/client/src/translations.js +16828 -16750
  23. package/package.json +10 -2
  24. package/src/config.js +2 -2
  25. package/src/constants.js +262 -4
  26. package/src/core.js +21 -3
  27. package/src/defaultModels.js +12 -12
  28. package/src/engine.js +7 -1
  29. package/src/events.js +4 -3
  30. package/src/filter.js +272 -260
  31. package/src/i18n.js +187 -177
  32. package/src/middlewares/middleware-mongodb.js +3 -1
  33. package/src/modules/assistant/assistant.js +131 -42
  34. package/src/modules/bucket.js +3 -2
  35. package/src/modules/data/data.backup.js +374 -0
  36. package/src/modules/data/data.core.js +2 -1
  37. package/src/modules/data/data.history.js +11 -8
  38. package/src/modules/data/data.js +190 -4551
  39. package/src/modules/data/data.operations.js +3000 -0
  40. package/src/modules/data/data.relations.js +687 -0
  41. package/src/modules/data/data.routes.js +132 -38
  42. package/src/modules/data/data.scheduling.js +274 -0
  43. package/src/modules/data/data.validation.js +248 -0
  44. package/src/modules/data/index.js +29 -1
  45. package/src/modules/user.js +2 -1
  46. package/src/modules/workflow.js +3 -2
  47. package/src/services/stripe.js +3 -2
  48. package/swagger-en.yml +133 -0
  49. package/test/model.integration.test.js +377 -221
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Data Primals Engine
1
+ # data-primals-engine
2
2
  [![Node.js CI](https://github.com/anonympins/data-primals-engine/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/anonympins/data-primals-engine/actions/workflows/node.js.yml)
3
3
  ![](https://img.shields.io/npm/dw/data-primals-engine)
4
4
  ![](https://img.shields.io/npm/last-update/data-primals-engine)
@@ -33,15 +33,6 @@
33
33
  - **📦 Starter Packs**: Quickly bootstrap applications with pre-built data packs for CRM, e-commerce, and more.
34
34
  - **📄Auto-Generated API Documentation**: Interactive API documentation available via the interface or at `/api-docs`.
35
35
 
36
- ## ⚛️ Frontend Integration (React)
37
-
38
- This engine is designed to work seamlessly with its dedicated client library, **`data-primals-engine/client`**. This library provides a complete set of React hooks and UI components to build a rich, data-centric user interface on top of the backend.
39
-
40
- While this README focuses on the backend engine and its API, you can find detailed instructions on how to integrate the client part in your React application here:
41
-
42
- ➡️ **[View the Frontend Integration Guide](https://github.com/anonympins/data-primals-engine/tree/develop/client)**
43
-
44
-
45
36
  ## 🌟 Why Choose data-primals-engine?
46
37
 
47
38
  - **Zero Boilerplate**: Focus on your business logic, not infrastructure
@@ -129,10 +120,11 @@ By default, the app runs on port **7633** : http://localhost:7633
129
120
 
130
121
  The engine includes a pluggable system for user management. For development and testing, it uses a `DefaultUserProvider` which creates a volatile **`demo`** user. This is perfect for getting started quickly without setting up a user database.
131
122
 
132
- For production environments, you should extend the base `UserProvider` class to connect to your actual user database (e.g., another MongoDB collection, a SQL database, or an external authentication service). This allows you to implement your own logic for finding users and validating passwords.
123
+ For production environments, you should use SSO providers as seen in the page below, or extend the base `UserProvider` class to connect to your actual user database (e.g., another MongoDB collection, a SQL database, or an external authentication service). This allows you to implement your own logic for finding and creating users.
124
+
125
+ ### Model generation
126
+ Models are the way to handle structured data. They organize data and they can be declared in JSON.
133
127
 
134
- ### Models
135
- Define schemas using JSON:
136
128
  ```json
137
129
  {
138
130
  "name": "product",
@@ -150,12 +142,8 @@ Define schemas using JSON:
150
142
  ]
151
143
  }
152
144
  ```
153
- ### Smart Relations
154
- - Handles up to 2,000 direct relations by default (can be customized)
155
- - For larger datasets, use intermediate collections
156
- - Automatic indexing on key fields
157
- - Custom indexing on fields
158
- - Custom fields :
145
+
146
+ ### Custom fields types
159
147
 
160
148
  | Type | Description | Properties/Notes |
161
149
  |:------------|:------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
@@ -180,6 +168,12 @@ Define schemas using JSON:
180
168
  | model | Stores a model by name | – |
181
169
  | modelField | Stores a model field path | – |
182
170
 
171
+ ### Other model features
172
+ - Handles up to 2,000 direct relations by default (can be customized). For larger datasets, use intermediate collections
173
+ - Automatic indexing on key fields
174
+ - Custom indexing on fields
175
+ - Anonymizable fields (encrypted for API users)
176
+
183
177
  ### Model constraints
184
178
  ```javascript
185
179
  {
@@ -250,6 +244,7 @@ data-primals-engine/
250
244
  ### 📁 Model Management
251
245
 
252
246
  #### Create a model
247
+ > Defines a new data model (schema) in the system. The request body must contain the model's name and an array of field definitions.
253
248
  ```bash
254
249
  curl -X POST http://localhost:7633/api/model?_user=demo \
255
250
  -H "Authorization: Bearer demotoken" \
@@ -265,6 +260,7 @@ curl -X POST http://localhost:7633/api/model?_user=demo \
265
260
  ```
266
261
 
267
262
  #### Update a model
263
+ > Modifies an existing model's structure using its unique ID. This allows you to add, remove, or change fields.
268
264
  ```bash
269
265
  curl -X PUT http://localhost:7633/api/model/60d0fe4f5311236168a109ca?_user=demo \
270
266
  -H "Authorization: Bearer demotoken" \
@@ -280,6 +276,7 @@ curl -X PUT http://localhost:7633/api/model/60d0fe4f5311236168a109ca?_user=demo
280
276
  ```
281
277
 
282
278
  #### Delete a model
279
+ > Permanently deletes a model definition using its name. This action is irreversible.
283
280
  ```bash
284
281
  curl -X DELETE "http://localhost:7633/api/model?_user=demo&name=newModel" \
285
282
  -H "Authorization: Bearer demotoken"
@@ -288,6 +285,7 @@ curl -X DELETE "http://localhost:7633/api/model?_user=demo&name=newModel" \
288
285
  ### 🗂️ Data Management
289
286
 
290
287
  #### Create a document
288
+ > Creates a single new document in the specified model. The `data` object must conform to the model's schema.
291
289
  ```bash
292
290
  curl -X POST http://localhost:7633/api/data?_user=demo \
293
291
  -H "Authorization: Bearer demotoken" \
@@ -303,18 +301,22 @@ curl -X POST http://localhost:7633/api/data?_user=demo \
303
301
  }'
304
302
  ```
305
303
 
304
+
306
305
  #### Search documents
306
+ > Searches for documents using a MongoDB-style filter. This endpoint is ideal for complex queries, pagination, and sorting.
307
+
307
308
  ```bash
308
309
  curl -X POST http://localhost:7633/api/data/search?_user=demo \
309
310
  -H "Authorization: Bearer demotoken" \
310
311
  -H "Content-Type: application/json" \
311
312
  -d '{
312
313
  "model": "product",
313
- "filter": { "price": { "$gt": 50 } }
314
+ "filter": { "$gt" : ["$price", 50] }
314
315
  }'
315
316
  ```
316
317
 
317
318
  #### Update a document by ID
319
+ > Updates a single document by its unique ID. The request body contains the fields to be modified.
318
320
  ```bash
319
321
  curl -X PUT http://localhost:7633/api/data/64a31c123ef59d4c8d55aa99?_user=demo \
320
322
  -H "Authorization: Bearer demotoken" \
@@ -325,7 +327,9 @@ curl -X PUT http://localhost:7633/api/data/64a31c123ef59d4c8d55aa99?_user=demo \
325
327
  }'
326
328
  ```
327
329
 
330
+
328
331
  #### Bulk update
332
+ > Updates multiple documents matching a filter. This is efficient for applying changes to a batch of records, such as restocking all out-of-stock products.
329
333
  ```bash
330
334
  curl -X PUT http://localhost:7633/api/data?_user=demo \
331
335
  -H "Authorization: Bearer demotoken" \
@@ -337,6 +341,7 @@ curl -X PUT http://localhost:7633/api/data?_user=demo \
337
341
  ```
338
342
 
339
343
  #### Delete documents
344
+ > Deletes one or more documents. You can provide an array of `ids` to delete specific documents.
340
345
  ```bash
341
346
  curl -X DELETE http://localhost:7633/api/data?_user=demo \
342
347
  -H "Authorization: Bearer demotoken" \
@@ -811,7 +816,7 @@ Event.Listen("OnDataAdded", (engine, data) => {
811
816
  | OnDataInsert | Triggered just before data insertion. It will use the overrided data. | System | internal | (data) |
812
817
  | OnDataValidate | Triggered to override validation check. | System | internal | (value, field, data) |
813
818
  | OnDataFilter | Triggered to override data filtering operation. | System | internal | (filteredValue, field, data) |
814
- | OnEmailTemplate | Triggered to override custom email templates | System | internal | (filteredValue, field, data) |
819
+ | OnEmailTemplate | Triggered to override custom email templates | System | internal | (templateData, lang) |
815
820
 
816
821
  ### Triggering events
817
822
 
@@ -826,6 +831,16 @@ Results are merged together if multiple events are triggered.
826
831
  - arrays are concatenated
827
832
  - objects are merged using spread operator
828
833
 
834
+ ---
835
+
836
+ ## ⚛️ Frontend Integration (React)
837
+
838
+ This engine is designed to work seamlessly with its dedicated client library, **`data-primals-engine/client`**. This library provides a complete set of React hooks and UI components to build a rich, data-centric user interface on top of the backend.
839
+
840
+ While this README focuses on the backend engine and its API, you can find detailed instructions on how to integrate the client part in your React application here:
841
+
842
+ ➡️ **[View the Frontend Integration Guide](https://github.com/anonympins/data-primals-engine/tree/develop/client)**
843
+
829
844
 
830
845
  ---
831
846
 
@@ -849,4 +864,4 @@ Distributed under the **MIT License**. See `LICENSE` file.
849
864
 
850
865
  ---
851
866
 
852
- ## [🔼](https://github.com/anonympins/data-primals-engine?tab=readme-ov-file#data-primals-engine) Back to Top
867
+ ## [🔼](#data-primals-engine) Back to Top
@@ -33,6 +33,7 @@
33
33
  "react": "^18.3.1",
34
34
  "react-big-calendar": "^1.15.0",
35
35
  "react-dom": "^18.3.1",
36
+ "react-leaflet": "^4.2.1",
36
37
  "react-router": "^7.8.1",
37
38
  "react-switch": "^7.1.0",
38
39
  "uniqid": "^5.4.0",
@@ -1692,6 +1693,17 @@
1692
1693
  "url": "https://opencollective.com/popperjs"
1693
1694
  }
1694
1695
  },
1696
+ "node_modules/@react-leaflet/core": {
1697
+ "version": "2.1.0",
1698
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
1699
+ "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
1700
+ "license": "Hippocratic-2.1",
1701
+ "peerDependencies": {
1702
+ "leaflet": "^1.9.0",
1703
+ "react": "^18.0.0",
1704
+ "react-dom": "^18.0.0"
1705
+ }
1706
+ },
1695
1707
  "node_modules/@remirror/core-constants": {
1696
1708
  "version": "3.0.0",
1697
1709
  "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
@@ -5157,6 +5169,13 @@
5157
5169
  "json-buffer": "3.0.1"
5158
5170
  }
5159
5171
  },
5172
+ "node_modules/leaflet": {
5173
+ "version": "1.9.4",
5174
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
5175
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
5176
+ "license": "BSD-2-Clause",
5177
+ "peer": true
5178
+ },
5160
5179
  "node_modules/levn": {
5161
5180
  "version": "0.4.1",
5162
5181
  "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -6274,6 +6293,20 @@
6274
6293
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
6275
6294
  "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
6276
6295
  },
6296
+ "node_modules/react-leaflet": {
6297
+ "version": "4.2.1",
6298
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
6299
+ "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
6300
+ "license": "Hippocratic-2.1",
6301
+ "dependencies": {
6302
+ "@react-leaflet/core": "^2.1.0"
6303
+ },
6304
+ "peerDependencies": {
6305
+ "leaflet": "^1.9.0",
6306
+ "react": "^18.0.0",
6307
+ "react-dom": "^18.0.0"
6308
+ }
6309
+ },
6277
6310
  "node_modules/react-lifecycles-compat": {
6278
6311
  "version": "3.0.4",
6279
6312
  "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
@@ -51,6 +51,7 @@
51
51
  "react": "^18.3.1",
52
52
  "react-big-calendar": "^1.15.0",
53
53
  "react-dom": "^18.3.1",
54
+ "react-leaflet": "^4.2.1",
54
55
  "react-router": "^7.8.1",
55
56
  "react-switch": "^7.1.0",
56
57
  "uniqid": "^5.4.0",
@@ -1039,10 +1039,18 @@ h2.shadow {
1039
1039
  }
1040
1040
  }
1041
1041
 
1042
- .mini input[type=checkbox] {
1043
- cursor: pointer;
1044
- width: 15px;
1045
- height: 15px;
1042
+ .mini {
1043
+ .field-checkbox.field-bg {
1044
+ background: none;
1045
+ }
1046
+ input[type=checkbox] {
1047
+ cursor: pointer;
1048
+ margin: 0 auto;
1049
+ background: none;
1050
+ width: 16px;
1051
+ height: 16px;
1052
+ display: block;
1053
+ }
1046
1054
  }
1047
1055
 
1048
1056
  .pagination-wrapper {
@@ -1,16 +1,26 @@
1
1
  // client/src/components/AssistantChat.jsx
2
2
 
3
3
  import React, { useState, useMemo, useEffect, useRef } from 'react';
4
- import { FaRobot, FaPaperPlane, FaTimes, FaExpand, FaCompress } from 'react-icons/fa';
4
+ import { FaRobot, FaPaperPlane, FaTimes, FaExpand, FaCompress, FaPlus } from 'react-icons/fa';
5
5
  import './AssistantChat.scss';
6
6
  import { useModelContext } from "./contexts/ModelContext.jsx";
7
7
  import { Trans, useTranslation } from "react-i18next";
8
8
  import Markdown from 'react-markdown';
9
9
  import {useQueryClient} from "react-query";
10
10
  import {providers} from "../../src/modules/assistant/constants.js";
11
+ import DashboardChart from "./DashboardChart.jsx";
12
+ import {useUI} from "./contexts/UIContext.jsx";
13
+ import Button from "./Button.jsx";
14
+ import {getUserHash, getUserId} from "../../src/data.js";
15
+ import {useNavigation} from "react-router";
16
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
17
+ import {useNavigate} from "react-router-dom";
18
+ import {DataTable} from "./DataTable.jsx";
11
19
 
12
20
  const AssistantChat = ({ config }) => {
13
- const { selectedModel } = useModelContext();
21
+ const { selectedModel, models } = useModelContext();
22
+ const { me } = useAuthContext();
23
+ const nav = useNavigate();
14
24
  const { t } = useTranslation();
15
25
  const [isOpen, setIsOpen] = useState(false);
16
26
  const [isMaximized, setIsMaximized] = useState(false);
@@ -19,6 +29,7 @@ const AssistantChat = ({ config }) => {
19
29
  ]);
20
30
  const [input, setInput] = useState('');
21
31
  const [isLoading, setIsLoading] = useState(false);
32
+ const { setChartToAdd } = useUI();
22
33
 
23
34
  // NOUVEL ÉTAT : Stocke une action en attente de confirmation de l'utilisateur
24
35
  const [pendingConfirmation, setPendingConfirmation] = useState(null);
@@ -74,7 +85,8 @@ const AssistantChat = ({ config }) => {
74
85
  const botMessage = {
75
86
  from: 'bot',
76
87
  text: null,
77
- actionDetails: null // Pour stocker les détails de l'action à afficher
88
+ actionDetails: null,
89
+ chartConfig: null// Pour stocker les détails de l'action à afficher
78
90
  };
79
91
 
80
92
  // Gérer le texte à afficher
@@ -104,7 +116,15 @@ const AssistantChat = ({ config }) => {
104
116
  }
105
117
 
106
118
  // On ajoute le message à la liste uniquement s'il a du contenu textuel
107
- if (botMessage.text) {
119
+ if (result.chartConfig) {
120
+ botMessage.chartConfig = result.chartConfig;
121
+ }
122
+ // Si des données tabulaires sont retournées
123
+ if (result.dataResult) {
124
+ botMessage.dataResult = result.dataResult;
125
+ }
126
+ // On ajoute le message à la liste uniquement s'il a du contenu textuel ou un graphique
127
+ if (botMessage.text || botMessage.chartConfig || botMessage.dataResult) {
108
128
  setMessages(prev => [...prev, botMessage]);
109
129
  }
110
130
 
@@ -225,7 +245,30 @@ const AssistantChat = ({ config }) => {
225
245
  <div className="chat-messages">
226
246
  {messages.map((msg, index) => (
227
247
  <div key={index} className={`message ${msg.from}`}>
228
- <Markdown>{msg.text}</Markdown>
248
+ {msg.text && <Markdown>{msg.text}</Markdown>}
249
+ {msg.chartConfig && (
250
+ <div className="chart-container">
251
+ <DashboardChart config={msg.chartConfig} />
252
+ <div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
253
+ <Button onClick={() => {
254
+ nav('/user/'+getUserHash(me)+'/dashboards');
255
+ setChartToAdd(msg.chartConfig);
256
+ }} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
257
+ <FaPlus />
258
+ <span style={{ marginLeft: '8px' }}>
259
+ {t('assistant.addToDashboard', 'Ajouter au tableau de bord')}
260
+ </span>
261
+ </Button>
262
+ </div>
263
+ </div>
264
+ )}
265
+
266
+ {/* NOUVEAU : Affichage des données tabulaires */}
267
+ {msg.dataResult && (
268
+ <div className="data-table-container">
269
+ <DataTable model={models.find(f => f.name === msg.dataResult.model)} advanced={false} data={msg.dataResult.data} />
270
+ </div>
271
+ )}
229
272
  {msg.actionDetails && (
230
273
  <div className="action-details">
231
274
  {msg.actionDetails.model && (
@@ -125,7 +125,6 @@ $input-height: 50px;
125
125
  gap: 12px;
126
126
 
127
127
  .message {
128
- max-width: 80%;
129
128
  p {
130
129
  padding: 4px 8px;
131
130
  border-radius: 18px;
@@ -55,6 +55,7 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
55
55
  const [chartType, setChartType] = useState('bar');
56
56
  const [chartAggregationType, setChartAggregationType] = useState('count');
57
57
  const [chartTitle, setChartTitle] = useState('');
58
+ const [timeUnit, setTimeUnit] = useState('day');
58
59
 
59
60
  const currentModel = models.find(f => f.name === selectedModel && f._user === me.username);
60
61
 
@@ -74,6 +75,7 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
74
75
  setColorField(initialConfig.chartBackgroundColor || null);
75
76
  setGroupByLabelField(initialConfig.groupByLabelField || '');
76
77
  setChartAggregationType(initialConfig.aggregationType || 'count');
78
+ setTimeUnit(initialConfig.timeUnit || 'day');
77
79
  // Note: modelFields et relatedModelFields seront chargés par les autres useEffects
78
80
  // déclenchés par le changement de selectedModel et groupByField.
79
81
  } else {
@@ -87,6 +89,7 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
87
89
  setGroupByField('');
88
90
  setGroupByLabelField('');
89
91
  setChartAggregationType('count');
92
+ setTimeUnit('day');
90
93
  setModelFields([]);
91
94
  setRelatedModelFields([]);
92
95
  }
@@ -155,6 +158,9 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
155
158
  const isRelationGroupBy = isGroupingChart && modelFields.find(f => f.name === groupByField)?.type === 'relation';
156
159
  const isYAxisRequiredForValidation = chartAggregationType && !['count'].includes(chartAggregationType);
157
160
 
161
+ const xAxisFieldDefinition = modelFields.find(f => f.name === xAxisField);
162
+ const isTemporal = !isGroupingChart && xAxisFieldDefinition && ['date', 'datetime'].includes(xAxisFieldDefinition.type);
163
+
158
164
  // handleSave (inchangé - envoie l'état actuel)
159
165
  const handleSave = () => {
160
166
  const isValid = selectedModel && chartType && chartTitle &&
@@ -178,6 +184,7 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
178
184
  aggregationType: chartAggregationType,
179
185
  chartBackgroundColor: colorField,
180
186
  filter,
187
+ timeUnit: isTemporal ? timeUnit : undefined,
181
188
  });
182
189
  } else {
183
190
  let errorMsg = t('chartConfigModal.fillFields', "Veuillez remplir tous les champs requis.");
@@ -321,15 +328,33 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
321
328
  )}
322
329
  </>
323
330
  ) : (
324
- <SelectField
325
- label={t('chartConfigModal.xAxis', 'Axe X')}
326
- value={xAxisField}
327
- onChange={(item) => setXAxisField(item.value)}
328
- items={[{label: t('selectPlaceholder', 'Choisir...'), value: ''}, ...xAxisOptions]}
329
- required={!isGroupingChart}
330
- disabled={xAxisOptions.length === 0}
331
- hint={xAxisOptions.length === 0 ? t('chartConfigModal.noXAxisFields', 'Aucun champ utilisable pour l\'axe X') : ''}
332
- />
331
+ <>
332
+ <SelectField
333
+ label={t('chartConfigModal.xAxis', 'Axe X')}
334
+ value={xAxisField}
335
+ onChange={(item) => setXAxisField(item.value)}
336
+ items={[{label: t('selectPlaceholder', 'Choisir...'), value: ''}, ...xAxisOptions]}
337
+ required={!isGroupingChart}
338
+ disabled={xAxisOptions.length === 0}
339
+ hint={xAxisOptions.length === 0 ? t('chartConfigModal.noXAxisFields', 'Aucun champ utilisable pour l\'axe X') : ''}
340
+ />
341
+ {isTemporal && (
342
+ <SelectField
343
+ label={t('charts.timeUnit', 'Échelle de temps')}
344
+ value={timeUnit}
345
+ onChange={(item) => setTimeUnit(item.value)}
346
+ items={[
347
+ { value: 'minute', label: t('time.minute', 'Minute') },
348
+ { value: 'hour', label: t('time.hour', 'Heure') },
349
+ { value: 'day', label: t('time.day', 'Jour') },
350
+ { value: 'week', label: t('time.week', 'Semaine') },
351
+ { value: 'month', label: t('time.month', 'Mois') },
352
+ { value: 'year', label: t('time.year', 'Année') },
353
+ ]}
354
+ hint={t('charts.timeUnitHelp', "Définit l'unité de regroupement pour l'axe des dates.")}
355
+ />
356
+ )}
357
+ </>
333
358
  )}
334
359
 
335
360
  {/* Axe Y (inchangé) */}
@@ -14,9 +14,9 @@ import './ConditionBuilder.scss';
14
14
  import { useTranslation, Trans } from "react-i18next";
15
15
  import { CodeField } from "./Field.jsx";
16
16
  import { useAuthContext } from "./contexts/AuthContext.jsx";
17
- import { MONGO_CALC_OPERATORS} from "./filter.js";
18
17
  import {ConditionBuilder2} from "./ConditionBuilder2.jsx";
19
18
  import {mongoOperators} from "./constants.js";
19
+ import {MONGO_CALC_OPERATORS} from "../../src/constants";
20
20
 
21
21
  const getFieldDefinition = (fields, fieldName) => fields?.find(f => f.name === fieldName);
22
22
  const getModelFields = (modelName, allModels, user) => {
@@ -2,10 +2,11 @@ import React, {useEffect, useState} from 'react';
2
2
  import { Tooltip } from 'react-tooltip';
3
3
  import {FaPlus, FaProjectDiagram, FaTrash, FaInfoCircle, FaEdit, FaTags, FaCalendarAlt} from 'react-icons/fa';
4
4
  import { Trans } from 'react-i18next';
5
- import {convertInputValue, MONGO_CALC_OPERATORS} from "./filter.js";
5
+ import {convertInputValue} from "./filter.js";
6
6
  import i18n from "i18next";
7
7
  import {FaRepeat} from "react-icons/fa6";
8
8
  import {useAuthContext} from "./contexts/AuthContext.jsx";
9
+ import {MONGO_CALC_OPERATORS} from "../../src/constants.js";
9
10
 
10
11
 
11
12
  // Déterminer si le champ doit être une date