data-primals-engine 1.2.3 → 1.2.4
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.
- package/CONTRIBUTING.md +91 -0
- package/README.md +33 -17
- package/client/src/App.jsx +0 -5
- package/client/src/ConditionBuilder.scss +34 -1
- package/client/src/ConditionBuilder2.jsx +179 -53
- package/client/src/ContentView.jsx +0 -3
- package/client/src/CronBuilder.jsx +0 -1
- package/client/src/CronPartBuilder.jsx +0 -2
- package/client/src/DashboardView.jsx +0 -5
- package/client/src/DataEditor.jsx +8 -10
- package/client/src/DataLayout.jsx +0 -1
- package/client/src/DataTable.jsx +1 -3
- package/client/src/Field.jsx +0 -5
- package/client/src/FlexBuilder.jsx +1 -1
- package/client/src/ModelCreatorField.jsx +1 -5
- package/client/src/RTE.jsx +1 -6
- package/client/src/RTETrans.jsx +0 -2
- package/client/src/RelationField.jsx +1 -1
- package/client/src/RelationValue.jsx +1 -2
- package/client/src/TourSpotlight.jsx +0 -2
- package/client/src/filter.js +87 -0
- package/client/src/hooks/data.js +1 -3
- package/client/src/hooks/useTutorials.jsx +0 -1
- package/package.json +3 -3
- package/server.js +2 -2
- package/src/email.js +2 -2
- package/src/engine.js +59 -20
- package/src/index.js +1 -1
- package/src/middlewares/middleware-mongodb.js +0 -1
- package/src/modules/assistant.js +1 -3
- package/src/modules/bucket.js +3 -4
- package/src/modules/{data.js → data/data.js} +34 -51
- package/src/modules/data/index.js +1 -0
- package/src/modules/file.js +1 -1
- package/src/modules/mongodb.js +0 -1
- package/src/modules/user.js +1 -1
- package/src/modules/workflow.js +38 -38
- package/src/packs.js +4 -1
- package/test/data.backup.integration.test.js +4 -5
- package/test/data.integration.test.js +2 -6
- package/test/events.test.js +1 -1
- package/test/file.test.js +1 -4
- package/test/import_export.integration.test.js +20 -13
- package/test/model.integration.test.js +8 -10
- package/test/user.test.js +2 -2
- package/test/vm.test.js +1 -1
- package/test/workflow.integration.test.js +17 -14
- package/test/workflow.robustness.test.js +15 -10
- package/src/modules/test +0 -147
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Contributing to Data Primals Engine
|
|
2
|
+
|
|
3
|
+
Hello and a huge thank you for your interest in `data-primals-engine`! 🎉
|
|
4
|
+
|
|
5
|
+
We're thrilled to see you want to help us build an even better tool. Every contribution, big or small, is valuable and appreciated. This document is here to guide you.
|
|
6
|
+
|
|
7
|
+
## Code of Conduct
|
|
8
|
+
|
|
9
|
+
To ensure a welcoming and collaborative environment, we expect all contributors to follow our Code of Conduct. In short: be respectful, constructive, and open-minded.
|
|
10
|
+
|
|
11
|
+
## How Can I Contribute?
|
|
12
|
+
|
|
13
|
+
There are many ways to contribute, and all are welcome:
|
|
14
|
+
|
|
15
|
+
### 🐛 Reporting Bugs
|
|
16
|
+
|
|
17
|
+
- **Check existing issues** to see if someone has already reported the same bug.
|
|
18
|
+
- If not, open a new issue.
|
|
19
|
+
- Describe the bug as precisely as possible:
|
|
20
|
+
- How to reproduce it?
|
|
21
|
+
- What was the expected behavior?
|
|
22
|
+
- What is the actual behavior?
|
|
23
|
+
- Include screenshots or logs if possible.
|
|
24
|
+
|
|
25
|
+
### 💡 Suggesting Enhancements
|
|
26
|
+
|
|
27
|
+
Have an idea for a new feature or an improvement?
|
|
28
|
+
- Open a new issue to discuss it.
|
|
29
|
+
- Clearly explain the "why": what problem does this feature solve?
|
|
30
|
+
- Describe the "how": how do you imagine it would work?
|
|
31
|
+
|
|
32
|
+
### ✍️ Creating a Module
|
|
33
|
+
|
|
34
|
+
Modules are the primary way to extend the engine's functionality. They can add new API endpoints, listen to events, or perform background tasks.
|
|
35
|
+
|
|
36
|
+
#### 1. File Structure
|
|
37
|
+
|
|
38
|
+
The recommended way to create a module is to add a new directory inside `src/modules/`. The engine will automatically discover it if you follow the conventions.
|
|
39
|
+
|
|
40
|
+
For a module named `greeter`:
|
|
41
|
+
```
|
|
42
|
+
+src/
|
|
43
|
+
+└── modules/
|
|
44
|
+
•└── greeter/
|
|
45
|
+
• ├── index.js # The module's entry point
|
|
46
|
+
• └── greeter-logic.js # (Optional) Other logic files
|
|
47
|
+
```
|
|
48
|
+
#### 2. The onInit function
|
|
49
|
+
+ The entry point of your module (index.js) must export an async function called onInit :
|
|
50
|
+
```javascript
|
|
51
|
+
let engine, logger;
|
|
52
|
+
export async function onInit(defaultEngine) {
|
|
53
|
+
engine = defaultEngine;
|
|
54
|
+
logger = engine.getComponent(Logger);
|
|
55
|
+
// ...
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
### 🚀 Submitting Pull Requests
|
|
59
|
+
|
|
60
|
+
If you're ready to write code, that's fantastic!
|
|
61
|
+
|
|
62
|
+
1. **Find an issue**: Look for issues with the `good first issue` or `help wanted` labels. Comment on the issue to let us know you're working on it.
|
|
63
|
+
2. **Fork the repository** to your own GitHub account.
|
|
64
|
+
3. **Clone your fork** locally: `git clone https://github.com/YOUR_USERNAME/data-primals-engine.git`
|
|
65
|
+
4. **Create a branch** for your changes: `git checkout -b feature/your-feature-name` or `fix/bug-description`.
|
|
66
|
+
5. **Make your changes**. Be sure to follow the project's style conventions.
|
|
67
|
+
6. **Start the development server** to test your changes: `npm run dev`.
|
|
68
|
+
7. **Commit your changes** with a clear message: `git commit -m "feat: Add feature X"`. (We follow the Conventional Commits specification).
|
|
69
|
+
8. **Push your branch** to your fork: `git push origin feature/your-feature-name`.
|
|
70
|
+
9. **Open a Pull Request** from your fork to the original repository's `main` branch.
|
|
71
|
+
10. **Link your PR** to the corresponding issue.
|
|
72
|
+
|
|
73
|
+
### 📖 Improving Documentation
|
|
74
|
+
|
|
75
|
+
Good documentation is essential. If you find a typo, an unclear explanation, or think a section is missing, please don't hesitate to suggest a change!
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## ✨ Our Awesome Contributors
|
|
80
|
+
|
|
81
|
+
This project wouldn't exist without the incredible people who have given their time and expertise. A huge thank you to each and every one of you!
|
|
82
|
+
|
|
83
|
+
<a href="https://github.com/anonympins/data-primals-engine/graphs/contributors">
|
|
84
|
+
<img src="https://contrib.rocks/image?repo=anonympins/data-primals-engine" />
|
|
85
|
+
</a>
|
|
86
|
+
|
|
87
|
+
*This list is updated automatically.*
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
Thanks again for your contribution! We can't wait to see what we'll build together.
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- **Event-Driven & Extensible**: A core event system allows for deep customization and the easy creation of new modules or plugins.
|
|
27
27
|
- **Authentication & Authorization**: Robust role-based access control (RBAC) and pluggable user providers.
|
|
28
28
|
- **Built-in File Management**: Handle file uploads seamlessly with integrated support for AWS S3 storage.
|
|
29
|
-
- **🧠 AI Integration**: Natively supports OpenAI and Google Gemini models via LangChain for content generation, analysis, and more.
|
|
29
|
+
- **🧠 AI Integration**: Natively supports OpenAI, DeepSeek and Google Gemini models via LangChain for content generation, analysis, and more.
|
|
30
30
|
- **🌐 Internationalization (i18n)**: Fully supports multilingual interfaces and user-specific translated data.
|
|
31
31
|
- **📦 Starter Packs**: Quickly bootstrap applications with pre-built data packs for CRM, e-commerce, and more.
|
|
32
32
|
- **📄Auto-Generated API Documentation**: Interactive API documentation available via the interface or at `/api-docs`.
|
|
@@ -182,6 +182,20 @@ Activatable features:
|
|
|
182
182
|
- Define your model
|
|
183
183
|
- Set up workflow: "When new entry → generate AI content"
|
|
184
184
|
|
|
185
|
+
### 📧 Email Campaign Management
|
|
186
|
+
The "Marketing & Campaigning" starter pack provides a powerful solution for sending large-scale email campaigns without overloading your server.
|
|
187
|
+
|
|
188
|
+
- **Install the Pack**: A single command installs the necessary models (`campaign`, `audience`) and a sophisticated workflow.
|
|
189
|
+
- **Dynamic Audiences**: Create target audiences with MongoDB filters. For example, select all contacts with the "newsletter" tag or located in a specific country.
|
|
190
|
+
- **Personalized Content**: Use variables like `{recipient.firstName}` in the subject and body of your emails for a personal touch.
|
|
191
|
+
- **Automated & Scalable Sending**: When you schedule a campaign, a pre-configured workflow takes over:
|
|
192
|
+
- It processes your audience in small batches (e.g., 10 recipients at a time).
|
|
193
|
+
- It sends emails to each batch and waits before processing the next, ensuring stability.
|
|
194
|
+
- It tracks processed recipients to avoid duplicates and allow the campaign to be safely paused and resumed.
|
|
195
|
+
- Once all emails are sent, the campaign is automatically marked as "completed".
|
|
196
|
+
|
|
197
|
+
This use case demonstrates how starter packs and workflows can automate complex, performance-critical business logic right out of the box.
|
|
198
|
+
|
|
185
199
|
---
|
|
186
200
|
|
|
187
201
|
## 🔌 API Examples (using `curl`)
|
|
@@ -290,8 +304,7 @@ curl -X DELETE http://localhost:7633/api/data?_user=demo \
|
|
|
290
304
|
Make sure you use the code below to initialize the user :
|
|
291
305
|
```javascript
|
|
292
306
|
import express from "express";
|
|
293
|
-
import {
|
|
294
|
-
import { insertData, searchData } from 'data-primals-engine/modules/data';
|
|
307
|
+
import {Engine, insertData, searchData } from './src/index';
|
|
295
308
|
|
|
296
309
|
// Ensure the engine is initialized
|
|
297
310
|
|
|
@@ -631,20 +644,23 @@ Event.Listen("OnDataAdded", (engine, data) => {
|
|
|
631
644
|
}, "event", "system");
|
|
632
645
|
```
|
|
633
646
|
|
|
634
|
-
| Event
|
|
635
|
-
|
|
636
|
-
| OnServerStart
|
|
637
|
-
| OnServerStop
|
|
638
|
-
| OnModelsLoaded
|
|
639
|
-
| OnModelsDeleted
|
|
640
|
-
| OnUserDataDumped | Triggered after a user's data has been backed up (dumped). | System
|
|
641
|
-
| OnDataRestored
|
|
642
|
-
| OnPackInstalled
|
|
643
|
-
| OnModelEdited
|
|
644
|
-
| OnDataAdded
|
|
645
|
-
| OnDataDeleted
|
|
646
|
-
| OnDataSearched
|
|
647
|
-
| OnDataExported
|
|
647
|
+
| Event | Description | Scope | Triggered by | Arguments (Payload) |
|
|
648
|
+
|:-----------------|:------------------------------------------------------------------------|:--------------|:---------------------|:-----------------------------------------------------------------------------------------------------------------------------------------|
|
|
649
|
+
| OnServerStart | Triggered once the HTTP server is started and listening. | System | engine.start() | engine |
|
|
650
|
+
| OnServerStop | Triggered right after the HTTP server is stopped. | System | engine.stop() | engine |
|
|
651
|
+
| OnModelsLoaded | Triggered after the initial models are loaded and validated at startup. | System | setupInitialModels() | engine, dbModels |
|
|
652
|
+
| OnModelsDeleted | Triggered after all models are deleted via the reset function. | System | engine.resetModels() | engine |
|
|
653
|
+
| OnUserDataDumped | Triggered after a user's data has been backed up (dumped). | System | jobDumpUserData() | engine |
|
|
654
|
+
| OnDataRestored | Triggered after a user's data has been restored from a backup. | System | loadFromDump() | (none) |
|
|
655
|
+
| OnPackInstalled | Triggered after a data pack has been successfully installed. | System | installPack() | pack |
|
|
656
|
+
| OnModelEdited | Triggered after a model definition has been modified. | System & User | editModel() | System: engine, newModel (Pipeline*)<br>User: newModel (or the version modified by the system) |
|
|
657
|
+
| OnDataAdded | Triggered after new data has been inserted. | System & User | insertData() | System: engine, insertedIds (Pipeline*)<br>User: insertedIds (or the version modified by the system) |
|
|
658
|
+
| OnDataDeleted | Triggered just after data is actually deleted. | System & User | deleteData() | System: engine, {model, filter} (Pipeline*)<br>User: {model, filter} |
|
|
659
|
+
| OnDataSearched | Triggered after a data search. | System & User | searchData() | System: engine, {data, count} (Pipeline*)<br>User: {data, count} (or the version modified by the system) |
|
|
660
|
+
| OnDataExported | Triggered after a data export. | System & User | exportData() | System: engine, exportResults, modelsToExport (Pipeline*)<br>User: exportResults, modelsToExport (or the version modified by the system) |
|
|
661
|
+
| OnDataInsert | Triggered just before data insertion. It will use the override data | System | internal | (data) |
|
|
662
|
+
| OnDataValidate | Triggered after a data internal validation check. | System | internal | (value, field, data) |
|
|
663
|
+
| OnDataFilter | Triggered after a data internal data filtering operation. | System | internal | (filteredValue, field, data) |
|
|
648
664
|
|
|
649
665
|
### Triggering events
|
|
650
666
|
|
package/client/src/App.jsx
CHANGED
|
@@ -61,7 +61,6 @@ import i18next from "i18next";
|
|
|
61
61
|
import {websiteTranslations} from "./translations.js";
|
|
62
62
|
|
|
63
63
|
import { Tooltip } from 'react-tooltip';
|
|
64
|
-
import {availableLangs} from "data-primals-engine/constants";
|
|
65
64
|
|
|
66
65
|
let queryClient = new QueryClient();
|
|
67
66
|
|
|
@@ -143,8 +142,6 @@ function Layout ({header, routes, body, footer}) {
|
|
|
143
142
|
google: availableKeys.find(key => key.name === 'GOOGLE_API_KEY')?.value || (useAI ? process.env.GOOGLE_API_KEY : undefined),
|
|
144
143
|
};
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
console.log(newConfig)
|
|
148
145
|
// On met à jour l'état si au moins une clé est disponible
|
|
149
146
|
if (newConfig.openai || newConfig.google) {
|
|
150
147
|
setAssistantConfig(newConfig);
|
|
@@ -305,7 +302,6 @@ function Layout ({header, routes, body, footer}) {
|
|
|
305
302
|
|
|
306
303
|
return () => {
|
|
307
304
|
if (eventSource) {
|
|
308
|
-
//console.log('[SSE] Fermeture de la connexion au serveur.');
|
|
309
305
|
eventSource.close();
|
|
310
306
|
}
|
|
311
307
|
};
|
|
@@ -581,7 +577,6 @@ const BaseLayout=()=>{
|
|
|
581
577
|
<h1 className="flex-1">{seoTitle}</h1>
|
|
582
578
|
<div className="center">
|
|
583
579
|
<SelectField label={<FaLanguage />} items={allLangs} onChange={(e) => {
|
|
584
|
-
console.log(e.value);
|
|
585
580
|
changeLanguage(e.value);
|
|
586
581
|
}} />
|
|
587
582
|
</div>
|
|
@@ -693,4 +693,37 @@
|
|
|
693
693
|
font-weight: bold;
|
|
694
694
|
margin-bottom: 4px;
|
|
695
695
|
color: #555;
|
|
696
|
-
}
|
|
696
|
+
}
|
|
697
|
+
.field-input-container {
|
|
698
|
+
position: relative;
|
|
699
|
+
display: flex;
|
|
700
|
+
align-items: center;
|
|
701
|
+
gap: 4px;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.date-input-wrapper {
|
|
705
|
+
display: flex;
|
|
706
|
+
align-items: center;
|
|
707
|
+
gap: 4px;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.toggle-date-mode {
|
|
711
|
+
background: none;
|
|
712
|
+
border: none;
|
|
713
|
+
cursor: pointer;
|
|
714
|
+
color: #666;
|
|
715
|
+
padding: 4px;
|
|
716
|
+
display: flex;
|
|
717
|
+
align-items: center;
|
|
718
|
+
justify-content: center;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.toggle-date-mode:hover {
|
|
722
|
+
color: #333;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
input[type="datetime-local"] {
|
|
726
|
+
padding: 6px;
|
|
727
|
+
border: 1px solid #ddd;
|
|
728
|
+
border-radius: 4px;
|
|
729
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, {useEffect, useState} from 'react';
|
|
2
2
|
import { Tooltip } from 'react-tooltip';
|
|
3
|
-
import {FaPlus, FaProjectDiagram, FaTrash, FaInfoCircle, FaEdit, FaTags} from 'react-icons/fa';
|
|
3
|
+
import {FaPlus, FaProjectDiagram, FaTrash, FaInfoCircle, FaEdit, FaTags, FaCalendarAlt} from 'react-icons/fa';
|
|
4
4
|
import { Trans } from 'react-i18next';
|
|
5
5
|
import {convertInputValue, MONGO_CALC_OPERATORS} from "./filter.js";
|
|
6
6
|
import i18n from "i18next";
|
|
@@ -19,8 +19,9 @@ const isDateArg = (operator, argIndex) => {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// Cas spécial pour les opérateurs de date
|
|
22
|
-
if (operator === '$dateAdd' || operator === '$dateSubtract'
|
|
23
|
-
|
|
22
|
+
if (operator === '$dateAdd' || operator === '$dateSubtract' ||
|
|
23
|
+
operator === '$dateDiff' || operator === '$dateToString') {
|
|
24
|
+
return true;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// Pour les autres opérateurs marqués isDate (comme $hour, $second, etc.)
|
|
@@ -118,7 +119,20 @@ const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = fals
|
|
|
118
119
|
regex: ""
|
|
119
120
|
}
|
|
120
121
|
};
|
|
121
|
-
}else if (opConfig.
|
|
122
|
+
} else if (opConfig.specialStructure) {
|
|
123
|
+
if (opConfig.args) {
|
|
124
|
+
// Initialiser seulement les champs obligatoires
|
|
125
|
+
const initialArgs = {};
|
|
126
|
+
opConfig.args.forEach(arg => {
|
|
127
|
+
if (!arg.optional) {
|
|
128
|
+
initialArgs[arg.name] = arg.type === 'select' ? (arg.options[0] || '') : '';
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
newValue = { [operator]: initialArgs };
|
|
132
|
+
} else {
|
|
133
|
+
newValue = { [operator]: {} };
|
|
134
|
+
}
|
|
135
|
+
} else if (opConfig.converter || (!opConfig.multi && !opConfig.args)) {
|
|
122
136
|
newValue = { [operator]: "" };
|
|
123
137
|
} else {
|
|
124
138
|
// Nouvelle logique qui prend en compte args et multi
|
|
@@ -141,9 +155,6 @@ const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = fals
|
|
|
141
155
|
setEditing(false);
|
|
142
156
|
};
|
|
143
157
|
|
|
144
|
-
console.log(path);
|
|
145
|
-
console.log(isDateArg(path[path.length - 1]));
|
|
146
|
-
|
|
147
158
|
const renderArgument = (arg, index, parentOperator, args) => {
|
|
148
159
|
const isSimpleValue = typeof arg !== 'object' || arg === null || Array.isArray(arg);
|
|
149
160
|
const parentOpConfig = MONGO_CALC_OPERATORS[parentOperator];
|
|
@@ -271,7 +282,6 @@ const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = fals
|
|
|
271
282
|
relatedFieldNames = relatedModel.fields.map(f => f.name);
|
|
272
283
|
relatedModelFields = relatedModel.fields;
|
|
273
284
|
}
|
|
274
|
-
console.log({models,relatedModel})
|
|
275
285
|
}
|
|
276
286
|
|
|
277
287
|
// --- Fin de la correction ---
|
|
@@ -454,7 +464,25 @@ const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = fals
|
|
|
454
464
|
}
|
|
455
465
|
|
|
456
466
|
if (opConfig.specialStructure) {
|
|
457
|
-
|
|
467
|
+
const handleSpecialArgChange = (argName, newValue) => {
|
|
468
|
+
const newArgs = {...args};
|
|
469
|
+
|
|
470
|
+
// Si la nouvelle valeur est vide et que l'argument est optionnel, on le supprime
|
|
471
|
+
if (newValue === '' || newValue === null || newValue === undefined) {
|
|
472
|
+
delete newArgs[argName];
|
|
473
|
+
} else {
|
|
474
|
+
newArgs[argName] = newValue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Si tous les arguments obligatoires sont vides, on supprime complètement
|
|
478
|
+
const hasRequiredArgs = opConfig.args?.some(a => !a.optional && newArgs[a.name]);
|
|
479
|
+
if (!hasRequiredArgs && Object.keys(newArgs).length === 0) {
|
|
480
|
+
onChange(null, path);
|
|
481
|
+
} else {
|
|
482
|
+
onChange({[operator]: newArgs}, path);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
458
486
|
return (
|
|
459
487
|
<div className="expression-block special-structure-block">
|
|
460
488
|
<div className="expression-header">
|
|
@@ -479,23 +507,66 @@ const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = fals
|
|
|
479
507
|
</div>
|
|
480
508
|
</div>
|
|
481
509
|
<div className="special-structure-args">
|
|
482
|
-
{
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
510
|
+
{opConfig.args ? (
|
|
511
|
+
opConfig.args.map((argConfig) => {
|
|
512
|
+
const currentArgValue = args[argConfig.name] || '';
|
|
513
|
+
return (
|
|
514
|
+
<div key={argConfig.name} className="special-arg">
|
|
515
|
+
<label>{argConfig.label}</label>
|
|
516
|
+
{argConfig.type === 'select' ? (
|
|
517
|
+
<select
|
|
518
|
+
value={currentArgValue}
|
|
519
|
+
onChange={(e) => handleSpecialArgChange(argConfig.name, e.target.value)}
|
|
520
|
+
>
|
|
521
|
+
<option value="">Select {argConfig.label}</option>
|
|
522
|
+
{argConfig.options.map(option => (
|
|
523
|
+
<option key={option} value={option}>{option}</option>
|
|
524
|
+
))}
|
|
525
|
+
</select>
|
|
526
|
+
) : argConfig.type === 'date' ? (
|
|
527
|
+
<input
|
|
528
|
+
type="datetime-local"
|
|
529
|
+
value={currentArgValue ? new Date(currentArgValue).toISOString().slice(0, 16) : ''}
|
|
530
|
+
onChange={(e) => {
|
|
531
|
+
const val = e.target.value;
|
|
532
|
+
handleSpecialArgChange(argConfig.name, val ? new Date(val).toISOString() : '');
|
|
533
|
+
}}
|
|
534
|
+
/>
|
|
535
|
+
) : (
|
|
536
|
+
<input
|
|
537
|
+
type={argConfig.type === 'number' ? 'number' : 'text'}
|
|
538
|
+
value={currentArgValue}
|
|
539
|
+
onChange={(e) => handleSpecialArgChange(argConfig.name, e.target.value)}
|
|
540
|
+
placeholder={argConfig.label}
|
|
541
|
+
/>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
);
|
|
545
|
+
})
|
|
546
|
+
) : (
|
|
547
|
+
// Ancien cas pour les structures spéciales non configurées
|
|
548
|
+
Object.entries(args).map(([key, val]) => (
|
|
549
|
+
<div key={key} className="special-arg">
|
|
550
|
+
<label>{key}</label>
|
|
551
|
+
<ExpressionField
|
|
552
|
+
value={val}
|
|
553
|
+
onChange={(newVal) => {
|
|
554
|
+
const newArgs = {...args, [key]: newVal};
|
|
555
|
+
// Supprimer si vide
|
|
556
|
+
if (newVal === '' || newVal === null || newVal === undefined) {
|
|
557
|
+
delete newArgs[key];
|
|
558
|
+
}
|
|
559
|
+
onChange(Object.keys(newArgs).length > 0 ? {[operator]: newArgs} : null, path);
|
|
560
|
+
}}
|
|
561
|
+
fieldNames={fieldNames}
|
|
562
|
+
path={[...path, operator, key]}
|
|
563
|
+
models={models}
|
|
564
|
+
currentModelFields={currentModelFields}
|
|
565
|
+
isInFindContext={isInFindContext}
|
|
566
|
+
/>
|
|
567
|
+
</div>
|
|
568
|
+
))
|
|
569
|
+
)}
|
|
499
570
|
</div>
|
|
500
571
|
</div>
|
|
501
572
|
);
|
|
@@ -670,11 +741,20 @@ const FieldInput = ({
|
|
|
670
741
|
const [inputValue, setInputValue] = useState(value);
|
|
671
742
|
const [suggestions, setSuggestions] = useState([]);
|
|
672
743
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
744
|
+
const [isDateMode, setIsDateMode] = useState(isDate);
|
|
673
745
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
|
674
746
|
|
|
675
747
|
useEffect(() => {
|
|
676
748
|
setInputValue(value);
|
|
677
|
-
|
|
749
|
+
// Détermine le mode d'affichage en fonction de la valeur
|
|
750
|
+
const shouldBeDateMode = isDate && (
|
|
751
|
+
// Soit c'est une date ISO valide
|
|
752
|
+
(typeof value === 'string' && !value.startsWith('$') && !isNaN(new Date(value).getTime())) ||
|
|
753
|
+
// Soit c'est une valeur vide et le champ est marqué comme date
|
|
754
|
+
(value === '' && isDate)
|
|
755
|
+
);
|
|
756
|
+
setIsDateMode(shouldBeDateMode);
|
|
757
|
+
}, [value, isDate]);
|
|
678
758
|
|
|
679
759
|
useEffect(() => {
|
|
680
760
|
// Filtrer les suggestions selon le contexte
|
|
@@ -706,6 +786,33 @@ const FieldInput = ({
|
|
|
706
786
|
onChange(val);
|
|
707
787
|
};
|
|
708
788
|
|
|
789
|
+
const handleDateChange = (dateValue) => {
|
|
790
|
+
// Convertir la date en format ISO
|
|
791
|
+
const isoDate = dateValue ? new Date(dateValue).toISOString() : '';
|
|
792
|
+
setInputValue(isoDate);
|
|
793
|
+
onChange(isoDate);
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
const toggleDateMode = () => {
|
|
798
|
+
const newMode = !isDateMode;
|
|
799
|
+
setIsDateMode(newMode);
|
|
800
|
+
|
|
801
|
+
if (newMode) {
|
|
802
|
+
// Si on passe en mode date et que la valeur actuelle est un champ, on la vide
|
|
803
|
+
if (inputValue && inputValue.startsWith('$')) {
|
|
804
|
+
setInputValue('');
|
|
805
|
+
onChange('');
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
// Si on quitte le mode date et que la valeur est une date, on la vide
|
|
809
|
+
if (inputValue && !isNaN(new Date(inputValue).getTime())) {
|
|
810
|
+
setInputValue('');
|
|
811
|
+
onChange('');
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
|
|
709
816
|
const handleSuggestionClick = (suggestion) => {
|
|
710
817
|
const prefix = isFieldName ? '' : (isInFindContext ? '$$this.' : '$');
|
|
711
818
|
const newValue = `${prefix}${suggestion}`;
|
|
@@ -714,36 +821,54 @@ const FieldInput = ({
|
|
|
714
821
|
setShowSuggestions(false);
|
|
715
822
|
};
|
|
716
823
|
|
|
717
|
-
// Si c'est un champ date, on affiche le datepicker
|
|
718
|
-
if (isDate) {
|
|
719
|
-
return (
|
|
720
|
-
<div className="date-input-container">
|
|
721
|
-
<input
|
|
722
|
-
type="datetime-local"
|
|
723
|
-
value={inputValue ? new Date(inputValue).toISOString().slice(0, 16) : ''}
|
|
724
|
-
onChange={(e) => handleDateChange(e.target.value)}
|
|
725
|
-
/>
|
|
726
|
-
</div>
|
|
727
|
-
);
|
|
728
|
-
}
|
|
729
824
|
return (
|
|
730
825
|
<div className="field-input-container">
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
826
|
+
{isDateMode ? (
|
|
827
|
+
<div className="date-input-wrapper">
|
|
828
|
+
<input
|
|
829
|
+
type="datetime-local"
|
|
830
|
+
value={inputValue && !isNaN(new Date(inputValue)) ? new Date(inputValue).toISOString().slice(0, 16) : ''}
|
|
831
|
+
onChange={(e) => handleDateChange(e.target.value)}
|
|
832
|
+
/>
|
|
833
|
+
<button
|
|
834
|
+
type="button"
|
|
835
|
+
onClick={toggleDateMode}
|
|
836
|
+
className="toggle-date-mode"
|
|
837
|
+
title={i18n.t("cb.switchToField")}
|
|
838
|
+
>
|
|
839
|
+
<FaEdit />
|
|
840
|
+
</button>
|
|
841
|
+
</div>
|
|
842
|
+
) : (
|
|
843
|
+
<>
|
|
844
|
+
<input
|
|
845
|
+
type="text"
|
|
846
|
+
value={inputValue}
|
|
847
|
+
onChange={handleInputChange}
|
|
848
|
+
placeholder={
|
|
849
|
+
onlyRelations ? i18n.t("cb.selectRelationField") :
|
|
850
|
+
isInFindContext ? i18n.t("cb.enterThisField") :
|
|
851
|
+
i18n.t("cb.enterValueOrField")
|
|
852
|
+
}
|
|
853
|
+
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
|
|
854
|
+
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
|
|
855
|
+
/>
|
|
856
|
+
{isDate && (
|
|
857
|
+
<button
|
|
858
|
+
type="button"
|
|
859
|
+
onClick={toggleDateMode}
|
|
860
|
+
className="toggle-date-mode"
|
|
861
|
+
title={i18n.t("cb.switchToDate")}
|
|
862
|
+
>
|
|
863
|
+
<FaCalendarAlt />
|
|
864
|
+
</button>
|
|
865
|
+
)}
|
|
866
|
+
</>
|
|
867
|
+
)}
|
|
868
|
+
|
|
743
869
|
{showSuggestions && suggestions.length > 0 && (
|
|
744
870
|
<div className="suggestions-dropdown">
|
|
745
871
|
{suggestions.map((suggestion, index) => {
|
|
746
|
-
// Trouver le champ complet pour afficher des infos supplémentaires
|
|
747
872
|
const fieldInfo = currentModelFields.find(f => f.name === suggestion);
|
|
748
873
|
return (
|
|
749
874
|
<div
|
|
@@ -751,7 +876,8 @@ const FieldInput = ({
|
|
|
751
876
|
className="suggestion-item"
|
|
752
877
|
onClick={() => handleSuggestionClick(suggestion)}
|
|
753
878
|
data-tooltip-id="field-tooltip"
|
|
754
|
-
data-tooltip-content={fieldInfo?.relation ?
|
|
879
|
+
data-tooltip-content={fieldInfo?.relation ?
|
|
880
|
+
i18n.t('cb.relationTooltip', { relation: fieldInfo.relation}) : ''}
|
|
755
881
|
>
|
|
756
882
|
{suggestion}
|
|
757
883
|
{fieldInfo?.relation && (
|
|
@@ -53,7 +53,6 @@ const ContentView = () => { // Le prop 'menu' a été retiré car non passé par
|
|
|
53
53
|
);
|
|
54
54
|
const pageContent = contentData?.data?.[0];
|
|
55
55
|
|
|
56
|
-
console.log({contentData})
|
|
57
56
|
// 4. Récupérer les sous-catégories de la catégorie parente 'cat'
|
|
58
57
|
const {data: childCategoriesData, isLoading: isLoadingChildCategories} = useQuery(
|
|
59
58
|
['childDocCategories', cat, langCode], // Clé de query mise à jour
|
|
@@ -87,7 +86,6 @@ const ContentView = () => { // Le prop 'menu' a été retiré car non passé par
|
|
|
87
86
|
const parentCategory = childCategories.find(c => c._id === itemCategoryId);
|
|
88
87
|
|
|
89
88
|
if (parentCategory) {
|
|
90
|
-
console.log({pt:parentCategory.name});
|
|
91
89
|
const categoryName = parentCategory.name.value || t('toc.unknownCategory', 'Autres');
|
|
92
90
|
if (grouped[categoryName]) { // La catégorie doit exister car initialisée plus haut
|
|
93
91
|
grouped[categoryName].push(item); // Les items sont déjà triés par la query
|
|
@@ -252,7 +250,6 @@ const ContentView = () => { // Le prop 'menu' a été retiré car non passé par
|
|
|
252
250
|
<APIInfo/>
|
|
253
251
|
) : root?.innerHTML.split("<pre><i>[@codeField]</i></pre>").map((m,index)=>{
|
|
254
252
|
const cc = codeFields[index];
|
|
255
|
-
console.log({cc, index});
|
|
256
253
|
return <div><div dangerouslySetInnerHTML={{__html: m}}></div>{cc && <div className={"fs-regular"}>
|
|
257
254
|
<CodeField
|
|
258
255
|
key={uniqid()}
|
|
@@ -38,7 +38,6 @@ const CronBuilder = ({ cronExpression, cronMask, defaultCronExpression, onCronCh
|
|
|
38
38
|
if (defaultParts.length === 5) {
|
|
39
39
|
defaultParts.unshift('*');
|
|
40
40
|
}
|
|
41
|
-
console.log({cronMask})
|
|
42
41
|
setParts({
|
|
43
42
|
second: cronMask && !cronMask[0] ? '0' : cronParts[0] || defaultParts[0] || '*',
|
|
44
43
|
minute: cronMask && !cronMask[1] ? '0' : cronParts[1] || defaultParts[1] || '*',
|
|
@@ -111,8 +111,6 @@ export const CronPartBuilder = ({ label, masked, value, defaultValue, onChange,
|
|
|
111
111
|
{ value: 'range', label: t('cron.range', `Plage (ex: 9-17)`) },
|
|
112
112
|
];
|
|
113
113
|
|
|
114
|
-
console.log({label, masked})
|
|
115
|
-
|
|
116
114
|
return (
|
|
117
115
|
<div className="cron-part-controls">
|
|
118
116
|
<SelectField disabled={masked} items={modeOptions} value={mode} onChange={handleModeChange} />
|
|
@@ -13,10 +13,6 @@ import { useQuery, useQueryClient, useMutation } from "react-query";
|
|
|
13
13
|
import { useAuthContext } from "./contexts/AuthContext.jsx";
|
|
14
14
|
import { DialogProvider } from "./Dialog.jsx";
|
|
15
15
|
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
16
|
-
import FlexDataRenderer from "./FlexDataRenderer.jsx";
|
|
17
|
-
import {conditionToApiSearchFilter} from "../../src/data.js";
|
|
18
|
-
// --- MODIFICATION : Import de la fonction cssProps ---
|
|
19
|
-
import { cssProps } from 'data-primals-engine/core';
|
|
20
16
|
import {DashboardFlexViewItem} from "./DashboardFlexViewItem.jsx";
|
|
21
17
|
|
|
22
18
|
// --- updateDashboardLayout (fonction utilitaire, peut rester ici ou être externalisée) ---
|
|
@@ -85,7 +81,6 @@ export function DashboardView({ dashboard }) {
|
|
|
85
81
|
(newLayout) => updateDashboardLayout(dashboard, newLayout, me.username, t),
|
|
86
82
|
{
|
|
87
83
|
onMutate: async (newLayout) => {
|
|
88
|
-
console.log("Optimi stic update:", newLayout);
|
|
89
84
|
const previousLayout = layoutState;
|
|
90
85
|
setLayoutState(newLayout);
|
|
91
86
|
return { previousLayout };
|