data-primals-engine 1.2.2 → 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.
Files changed (54) hide show
  1. package/CONTRIBUTING.md +91 -0
  2. package/README.md +33 -17
  3. package/client/src/App.jsx +0 -5
  4. package/client/src/ConditionBuilder.scss +34 -1
  5. package/client/src/ConditionBuilder2.jsx +179 -53
  6. package/client/src/ContentView.jsx +0 -3
  7. package/client/src/CronBuilder.jsx +0 -1
  8. package/client/src/CronPartBuilder.jsx +0 -2
  9. package/client/src/DashboardView.jsx +0 -5
  10. package/client/src/DataEditor.jsx +8 -211
  11. package/client/src/DataLayout.jsx +0 -1
  12. package/client/src/DataTable.jsx +1 -3
  13. package/client/src/Field.jsx +0 -5
  14. package/client/src/FlexBuilder.jsx +1 -1
  15. package/client/src/ModelCreatorField.jsx +1 -5
  16. package/client/src/RTE.jsx +1 -6
  17. package/client/src/RTETrans.jsx +0 -2
  18. package/client/src/RelationField.jsx +1 -1
  19. package/client/src/RelationValue.jsx +1 -2
  20. package/client/src/TourSpotlight.jsx +0 -2
  21. package/client/src/constants.js +1 -1
  22. package/client/src/filter.js +87 -0
  23. package/client/src/hooks/data.js +1 -3
  24. package/client/src/hooks/useTutorials.jsx +0 -1
  25. package/package.json +3 -3
  26. package/server.js +2 -2
  27. package/src/constants.js +2 -2
  28. package/src/defaultModels.js +1 -14
  29. package/src/email.js +9 -7
  30. package/src/engine.js +60 -20
  31. package/src/events.js +1 -1
  32. package/src/filter.js +221 -0
  33. package/src/index.js +1 -1
  34. package/src/middlewares/middleware-mongodb.js +0 -1
  35. package/src/modules/assistant.js +1 -3
  36. package/src/modules/bucket.js +3 -4
  37. package/src/modules/{data.js → data/data.js} +42 -59
  38. package/src/modules/data/index.js +1 -0
  39. package/src/modules/file.js +1 -1
  40. package/src/modules/mongodb.js +0 -1
  41. package/src/modules/user.js +1 -1
  42. package/src/modules/workflow.js +299 -133
  43. package/src/packs.js +249 -8
  44. package/test/data.backup.integration.test.js +7 -5
  45. package/test/data.integration.test.js +8 -6
  46. package/test/events.test.js +1 -1
  47. package/test/file.test.js +11 -17
  48. package/test/import_export.integration.test.js +38 -27
  49. package/test/model.integration.test.js +20 -21
  50. package/test/user.test.js +32 -25
  51. package/test/vm.test.js +51 -0
  52. package/test/workflow.integration.test.js +22 -14
  53. package/test/workflow.robustness.test.js +19 -9
  54. package/src/modules/test +0 -147
@@ -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 { Engine } from 'data-primals-engine/engine';
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 | Description | Scope | Triggered by | Arguments (Payload) |
635
- | :--- |:------------------------------------------------------------------------| :--- | :--- | :--- |
636
- | OnServerStart | Triggered once the HTTP server is started and listening. | System | engine.start() | engine |
637
- | OnServerStop | Triggered right after the HTTP server is stopped. | System | engine.stop() | engine |
638
- | OnModelsLoaded | Triggered after the initial models are loaded and validated at startup. | System | setupInitialModels() | engine, dbModels |
639
- | OnModelsDeleted | Triggered after all models are deleted via the reset function. | System | engine.resetModels() | engine |
640
- | OnUserDataDumped | Triggered after a user's data has been backed up (dumped). | System | jobDumpUserData() | engine |
641
- | OnDataRestored | Triggered after a user's data has been restored from a backup. | System | loadFromDump() | (none) |
642
- | OnPackInstalled | Triggered after a data pack has been successfully installed. | System | installPack() | pack |
643
- | 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) |
644
- | 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) |
645
- | OnDataDeleted | Triggered just after data is actually deleted. | System & User | deleteData() | System: engine, {model, filter} (Pipeline*)<br>User: {model, filter} |
646
- | 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) |
647
- | 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) |
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
 
@@ -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
- return argIndex === 0; // Seul le premier argument est une date
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.converter || (!opConfig.multi && !opConfig.args)) {
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
- // Cas spécial pour les opérateurs comme $regexMatch
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
- {Object.entries(args).map(([key, val]) => (
483
- <div key={key} className="special-arg">
484
- <label>{key}</label>
485
- <ExpressionField
486
- value={val}
487
- onChange={(newVal) => {
488
- const newArgs = {...args, [key]: newVal};
489
- onChange({[operator]: newArgs}, path);
490
- }}
491
- fieldNames={fieldNames}
492
- path={[...path, operator, key]}
493
- models={models}
494
- currentModelFields={currentModelFields}
495
- isInFindContext={isInFindContext}
496
- />
497
- </div>
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
- }, [value]);
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
- <input
732
- type="text"
733
- value={inputValue}
734
- onChange={handleInputChange}
735
- placeholder={
736
- onlyRelations ? i18n.t("cb.selectRelationField") :
737
- isInFindContext ?i18n.t("cb.enterThisField") :
738
- i18n.t("cb.enterValueOrField")
739
- }
740
- onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
741
- onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
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 ? i18n.t('cb.relationTooltip', { relation: 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 };