data-primals-engine 1.5.2 → 1.6.1

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 (70) hide show
  1. package/README.md +938 -915
  2. package/client/README.md +136 -136
  3. package/client/index.js +0 -1
  4. package/client/public/doc/API.postman_collection.json +213 -213
  5. package/client/public/react.svg +9 -9
  6. package/client/src/AddWidgetTypeModal.jsx +47 -47
  7. package/client/src/App.css +42 -42
  8. package/client/src/App.jsx +92 -150
  9. package/client/src/App.scss +6 -0
  10. package/client/src/AssistantChat.jsx +362 -363
  11. package/client/src/Button.jsx +12 -12
  12. package/client/src/Dashboard.jsx +480 -480
  13. package/client/src/DashboardHtmlViewItem.jsx +146 -146
  14. package/client/src/DashboardView.jsx +654 -654
  15. package/client/src/DataEditor.jsx +383 -383
  16. package/client/src/DataLayout.jsx +779 -808
  17. package/client/src/DataTable.jsx +782 -822
  18. package/client/src/DataTable.scss +186 -186
  19. package/client/src/GeolocationField.jsx +93 -93
  20. package/client/src/HistoryDialog.jsx +307 -307
  21. package/client/src/HistoryDialog.scss +319 -319
  22. package/client/src/HtmlViewBuilderModal.jsx +90 -90
  23. package/client/src/HtmlViewBuilderModal.scss +17 -17
  24. package/client/src/HtmlViewCard.jsx +43 -43
  25. package/client/src/ModelCreator.jsx +683 -686
  26. package/client/src/ModelCreator.scss +1 -1
  27. package/client/src/ModelCreatorField.jsx +950 -950
  28. package/client/src/ModelImporter.jsx +3 -0
  29. package/client/src/ModelList.jsx +280 -280
  30. package/client/src/Notification.jsx +136 -136
  31. package/client/src/PackGallery.jsx +391 -391
  32. package/client/src/PackGallery.scss +231 -231
  33. package/client/src/Pagination.jsx +143 -143
  34. package/client/src/RelationField.jsx +354 -354
  35. package/client/src/RelationSelectorWidget.jsx +172 -172
  36. package/client/src/RelationValue.jsx +2 -1
  37. package/client/src/contexts/CommandContext.jsx +260 -0
  38. package/client/src/contexts/ModelContext.jsx +4 -1
  39. package/client/src/contexts/UIContext.jsx +72 -72
  40. package/client/src/filter.js +263 -263
  41. package/client/src/index.css +90 -90
  42. package/package.json +6 -5
  43. package/perf/artillery-hooks.js +37 -37
  44. package/perf/setup.yml +25 -25
  45. package/src/constants.js +4 -0
  46. package/src/defaultModels.js +7 -0
  47. package/src/engine.js +335 -335
  48. package/src/events.js +232 -137
  49. package/src/filter.js +274 -274
  50. package/src/index.js +1 -0
  51. package/src/modules/assistant/assistant.js +225 -83
  52. package/src/modules/auth-google/index.js +50 -50
  53. package/src/modules/auth-microsoft/index.js +81 -81
  54. package/src/modules/data/data.core.js +118 -118
  55. package/src/modules/data/data.history.js +555 -555
  56. package/src/modules/data/data.operations.js +112 -9
  57. package/src/modules/data/data.relations.js +686 -686
  58. package/src/modules/data/data.routes.js +1879 -1879
  59. package/src/modules/data/data.validation.js +2 -2
  60. package/src/modules/file.js +247 -247
  61. package/src/modules/user.js +14 -9
  62. package/src/packs.js +2 -2
  63. package/src/providers.js +2 -2
  64. package/src/services/stripe.js +196 -196
  65. package/src/sso.js +193 -193
  66. package/test/data.history.integration.test.js +264 -264
  67. package/test/data.integration.test.js +1281 -1206
  68. package/test/events.test.js +108 -10
  69. package/test/model.integration.test.js +377 -377
  70. package/test/user.test.js +106 -1
@@ -1,173 +1,173 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useMutation, useQueryClient } from 'react-query';
3
- import { ModelProvider, useModelContext } from './contexts/ModelContext.jsx';
4
- import { DataTable } from './DataTable.jsx';
5
- import { DataEditor } from './DataEditor.jsx';
6
- import Button from './Button.jsx';
7
- import { useTranslation } from 'react-i18next';
8
- import { FaCheck, FaTimes, FaPlus, FaSearch } from 'react-icons/fa';
9
- import { getDefaultForType } from '../../src/data.js';
10
- import { useAuthContext } from './contexts/AuthContext.jsx';
11
-
12
- // These API functions can be moved to a dedicated service file later.
13
- const insertDataAPI = async (modelName, data) => {
14
- const response = await fetch(`/api/data`, {
15
- method: 'POST',
16
- headers: { 'Content-Type': 'application/json' },
17
- body: JSON.stringify({model: modelName,data})
18
- });
19
- if (!response.ok) {
20
- const error = await response.json();
21
- throw new Error(error.error || 'Failed to insert data');
22
- }
23
- return response.json();
24
- };
25
-
26
- const searchDataAPI = async (modelName, filter) => {
27
- const params = new URLSearchParams({ model: modelName, depth: '1' });
28
- const response = await fetch(`/api/data/search?${params.toString()}`, {
29
- method: 'POST',
30
- headers: { 'Content-Type': 'application/json' },
31
- body: JSON.stringify({ filter })
32
- });
33
- if (!response.ok) {
34
- const error = await response.json();
35
- throw new Error(error.error || 'Failed to search data');
36
- }
37
- return response.json();
38
- };
39
-
40
- const RelationSelectorWidget = ({ modelName, initialSelection, isMultiple, onValidate, onCancel }) => {
41
- const { t } = useTranslation();
42
- const { models, setSelectedModel } = useModelContext();
43
- const queryClient = useQueryClient();
44
- const { me } = useAuthContext();
45
- const [isCreating, setIsCreating] = useState(false);
46
- const [newRecord, setNewRecord] = useState(null);
47
-
48
- const [filterValues, setFilterValues] = useState({});
49
-
50
- // DataTable's `checkedItems` expects an array of objects with at least an `_id` property.
51
- // `initialSelection` is an array of string IDs. We convert it.
52
- const [selection, setSelection] = useState(() => initialSelection.map(id => ({ _id: id })));
53
-
54
- const model = models.find(m => m.name === modelName);
55
-
56
- useEffect(() => {
57
- if (model) {
58
- setSelectedModel(model);
59
- }
60
- }, [model, setSelectedModel]);
61
-
62
- const mutation = useMutation(
63
- (formData) => insertDataAPI(model.name, formData),
64
- {
65
- onSuccess: async (result) => {
66
- if (result.success && result.insertedIds?.length > 0) {
67
- queryClient.invalidateQueries(['api/data', modelName, 'page']);
68
- const newId = result.insertedIds[0];
69
- const searchResult = await searchDataAPI(model.name, { _id: newId });
70
- if (searchResult.data && searchResult.data.length > 0) {
71
- handleCreationSuccess(searchResult.data[0]);
72
- } else {
73
- console.error('Could not fetch the newly created item.');
74
- handleCancelCreation();
75
- }
76
- } else {
77
- console.error('Creation failed:', result.error);
78
- }
79
- },
80
- onError: (error) => {
81
- console.error('Creation mutation failed:', error.message);
82
- }
83
- }
84
- );
85
-
86
- const handleCreationSuccess = (newItem) => {
87
- setIsCreating(false);
88
- setNewRecord(null);
89
- if (isMultiple) {
90
- setSelection(current => [...current, newItem]);
91
- } else {
92
- setSelection([newItem]);
93
- }
94
- };
95
-
96
- const handleStartCreation = () => {
97
- const defaults = model.fields.reduce((acc, field) => {
98
- acc[field.name] = getDefaultForType(field);
99
- return acc;
100
- }, {});
101
- setNewRecord(defaults);
102
- setIsCreating(true);
103
- };
104
-
105
- const handleCancelCreation = () => {
106
- setIsCreating(false);
107
- setNewRecord(null);
108
- };
109
-
110
- const handleSave = (formData) => {
111
- mutation.mutate(formData);
112
- };
113
-
114
- if (!model) {
115
- return <div>{t('loading', 'Chargement...')}</div>;
116
- }
117
-
118
- const handleValidate = () => {
119
- // `onValidate` expects an array of full objects to update the display correctly.
120
- onValidate(selection);
121
- };
122
-
123
- const handleCheckboxChange = (items) => {
124
- if (!isMultiple) {
125
- // For single selection, we only keep the last selected item.
126
- // DataTable's setCheckedItems gives the full list.
127
- const lastItem = items.length > 0 ? [items[items.length - 1]] : [];
128
- setSelection(lastItem);
129
- } else {
130
- setSelection(items);
131
- }
132
- };
133
-
134
- if (isCreating) {
135
- return (
136
- <div className="relation-selector-widget">
137
- <Button onClick={handleCancelCreation} className="btn-secondary">
138
- <FaSearch /> {t('btns.backToSearch', 'Retour à la recherche')}
139
- </Button>
140
- <ModelProvider>
141
- <DataEditor
142
- model={model}
143
- formData={newRecord}
144
- setFormData={setNewRecord}
145
- onSubmit={handleSave}
146
- onCancel={handleCancelCreation}
147
- isLoading={mutation.isLoading}
148
- hideNewButton={true}
149
- />
150
- </ModelProvider>
151
- </div>
152
- );
153
- }
154
-
155
- return (
156
- <div className="relation-selector-widget">
157
- <DataTable model={model} filterValues={filterValues} setFilterValues={setFilterValues} checkedItems={selection} setCheckedItems={handleCheckboxChange} selectionMode={true} />
158
- <div className="flex actions right">
159
- <Button onClick={handleStartCreation} className="btn-secondary" style={{ marginRight: 'auto' }}>
160
- <FaPlus /> {t('btns.create', 'Créer')}
161
- </Button>
162
- <Button onClick={onCancel} className="btn-secondary">
163
- <FaTimes /> {t('btns.cancel', 'Annuler')}
164
- </Button>
165
- <Button onClick={handleValidate} className="btn-primary">
166
- <FaCheck /> {t('btns.validate', 'Valider')}
167
- </Button>
168
- </div>
169
- </div>
170
- );
171
- };
172
-
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useMutation, useQueryClient } from 'react-query';
3
+ import { ModelProvider, useModelContext } from './contexts/ModelContext.jsx';
4
+ import { DataTable } from './DataTable.jsx';
5
+ import { DataEditor } from './DataEditor.jsx';
6
+ import Button from './Button.jsx';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { FaCheck, FaTimes, FaPlus, FaSearch } from 'react-icons/fa';
9
+ import { getDefaultForType } from '../../src/data.js';
10
+ import { useAuthContext } from './contexts/AuthContext.jsx';
11
+
12
+ // These API functions can be moved to a dedicated service file later.
13
+ const insertDataAPI = async (modelName, data) => {
14
+ const response = await fetch(`/api/data`, {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({model: modelName,data})
18
+ });
19
+ if (!response.ok) {
20
+ const error = await response.json();
21
+ throw new Error(error.error || 'Failed to insert data');
22
+ }
23
+ return response.json();
24
+ };
25
+
26
+ const searchDataAPI = async (modelName, filter) => {
27
+ const params = new URLSearchParams({ model: modelName, depth: '1' });
28
+ const response = await fetch(`/api/data/search?${params.toString()}`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({ filter })
32
+ });
33
+ if (!response.ok) {
34
+ const error = await response.json();
35
+ throw new Error(error.error || 'Failed to search data');
36
+ }
37
+ return response.json();
38
+ };
39
+
40
+ const RelationSelectorWidget = ({ modelName, initialSelection, isMultiple, onValidate, onCancel }) => {
41
+ const { t } = useTranslation();
42
+ const { models, setSelectedModel } = useModelContext();
43
+ const queryClient = useQueryClient();
44
+ const { me } = useAuthContext();
45
+ const [isCreating, setIsCreating] = useState(false);
46
+ const [newRecord, setNewRecord] = useState(null);
47
+
48
+ const [filterValues, setFilterValues] = useState({});
49
+
50
+ // DataTable's `checkedItems` expects an array of objects with at least an `_id` property.
51
+ // `initialSelection` is an array of string IDs. We convert it.
52
+ const [selection, setSelection] = useState(() => initialSelection.map(id => ({ _id: id })));
53
+
54
+ const model = models.find(m => m.name === modelName);
55
+
56
+ useEffect(() => {
57
+ if (model) {
58
+ setSelectedModel(model);
59
+ }
60
+ }, [model, setSelectedModel]);
61
+
62
+ const mutation = useMutation(
63
+ (formData) => insertDataAPI(model.name, formData),
64
+ {
65
+ onSuccess: async (result) => {
66
+ if (result.success && result.insertedIds?.length > 0) {
67
+ queryClient.invalidateQueries(['api/data', modelName, 'page']);
68
+ const newId = result.insertedIds[0];
69
+ const searchResult = await searchDataAPI(model.name, { _id: newId });
70
+ if (searchResult.data && searchResult.data.length > 0) {
71
+ handleCreationSuccess(searchResult.data[0]);
72
+ } else {
73
+ console.error('Could not fetch the newly created item.');
74
+ handleCancelCreation();
75
+ }
76
+ } else {
77
+ console.error('Creation failed:', result.error);
78
+ }
79
+ },
80
+ onError: (error) => {
81
+ console.error('Creation mutation failed:', error.message);
82
+ }
83
+ }
84
+ );
85
+
86
+ const handleCreationSuccess = (newItem) => {
87
+ setIsCreating(false);
88
+ setNewRecord(null);
89
+ if (isMultiple) {
90
+ setSelection(current => [...current, newItem]);
91
+ } else {
92
+ setSelection([newItem]);
93
+ }
94
+ };
95
+
96
+ const handleStartCreation = () => {
97
+ const defaults = model.fields.reduce((acc, field) => {
98
+ acc[field.name] = getDefaultForType(field);
99
+ return acc;
100
+ }, {});
101
+ setNewRecord(defaults);
102
+ setIsCreating(true);
103
+ };
104
+
105
+ const handleCancelCreation = () => {
106
+ setIsCreating(false);
107
+ setNewRecord(null);
108
+ };
109
+
110
+ const handleSave = (formData) => {
111
+ mutation.mutate(formData);
112
+ };
113
+
114
+ if (!model) {
115
+ return <div>{t('loading', 'Chargement...')}</div>;
116
+ }
117
+
118
+ const handleValidate = () => {
119
+ // `onValidate` expects an array of full objects to update the display correctly.
120
+ onValidate(selection);
121
+ };
122
+
123
+ const handleCheckboxChange = (items) => {
124
+ if (!isMultiple) {
125
+ // For single selection, we only keep the last selected item.
126
+ // DataTable's setCheckedItems gives the full list.
127
+ const lastItem = items.length > 0 ? [items[items.length - 1]] : [];
128
+ setSelection(lastItem);
129
+ } else {
130
+ setSelection(items);
131
+ }
132
+ };
133
+
134
+ if (isCreating) {
135
+ return (
136
+ <div className="relation-selector-widget">
137
+ <Button onClick={handleCancelCreation} className="btn-secondary">
138
+ <FaSearch /> {t('btns.backToSearch', 'Retour à la recherche')}
139
+ </Button>
140
+ <ModelProvider>
141
+ <DataEditor
142
+ model={model}
143
+ formData={newRecord}
144
+ setFormData={setNewRecord}
145
+ onSubmit={handleSave}
146
+ onCancel={handleCancelCreation}
147
+ isLoading={mutation.isLoading}
148
+ hideNewButton={true}
149
+ />
150
+ </ModelProvider>
151
+ </div>
152
+ );
153
+ }
154
+
155
+ return (
156
+ <div className="relation-selector-widget">
157
+ <DataTable model={model} filterValues={filterValues} setFilterValues={setFilterValues} checkedItems={selection} setCheckedItems={handleCheckboxChange} selectionMode={true} />
158
+ <div className="flex actions right">
159
+ <Button onClick={handleStartCreation} className="btn-secondary" style={{ marginRight: 'auto' }}>
160
+ <FaPlus /> {t('btns.create', 'Créer')}
161
+ </Button>
162
+ <Button onClick={onCancel} className="btn-secondary">
163
+ <FaTimes /> {t('btns.cancel', 'Annuler')}
164
+ </Button>
165
+ <Button onClick={handleValidate} className="btn-primary">
166
+ <FaCheck /> {t('btns.validate', 'Valider')}
167
+ </Button>
168
+ </div>
169
+ </div>
170
+ );
171
+ };
172
+
173
173
  export default RelationSelectorWidget;
@@ -52,7 +52,8 @@ const RelationValue = ({ field, data, align }) => {
52
52
  });
53
53
  },
54
54
  { enabled: !!model && !!value, onSettled: (data) => {
55
-
55
+ if( !data)
56
+ return;
56
57
  const dt = {...relations};
57
58
  dt['api/data'+model.name+getObjectHash({ids})] = { count: data.length, data };
58
59
  setRelations(dt);
@@ -0,0 +1,260 @@
1
+ import React, {createContext, useContext, useState, useCallback, useRef, useEffect, useMemo} from 'react';
2
+ import { useQueryClient } from 'react-query';
3
+ import { useNotificationContext } from '../NotificationProvider.jsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import {elementsPerPage} from "../../../src/constants.js";
6
+ import {useModelContext} from "./ModelContext.jsx";
7
+
8
+ const CommandContext = createContext({});
9
+
10
+ export const useCommand = () => useContext(CommandContext);
11
+
12
+ // --- Command Manager ---
13
+ class CommandManager {
14
+ constructor() {
15
+ this.history = [];
16
+ this.currentIndex = -1;
17
+ // Les dépendances seront injectées via updateDependencies
18
+ this.addNotification = () => {};
19
+ this.t = (key) => key;
20
+ this.onResetQueryClient = () => {};
21
+ this.queryClient = null; // Ajout pour stocker le queryClient
22
+ }
23
+
24
+ updateDependencies(addNotification, t, onResetQueryClient, queryClient) {
25
+ this.addNotification = addNotification;
26
+ this.t = t;
27
+ this.onResetQueryClient = onResetQueryClient;
28
+ this.queryClient = queryClient; // Injection du client
29
+ }
30
+
31
+ async execute(command) {
32
+ try {
33
+ // Supprimer l'historique "redo" si on exécute une nouvelle commande
34
+ this.history = this.history.slice(0, this.currentIndex + 1);
35
+ await command.execute();
36
+ this.history.push(command);
37
+ this.currentIndex++;
38
+ await this.invalidateQueries(command.modelName, command.constructor.name);
39
+ // On vide la sélection APRÈS avoir invalidé, pour éviter les conflits de rendu.
40
+ if (command instanceof DeleteCommand && this.context?.setCheckedItems) {
41
+ this.context.setCheckedItems([]);
42
+ }
43
+ this.addNotification({ title: command.successMessage, status: 'completed' });
44
+ } catch (error) {
45
+ console.error("Command execution failed:", error);
46
+ this.addNotification({ title: this.t('command.error.execute', 'Erreur d\'exécution'), message: error.message, status: 'error' });
47
+ }
48
+ }
49
+
50
+ async undo() {
51
+ if (this.canUndo()) {
52
+ try {
53
+ const command = this.history[this.currentIndex];
54
+ await command.undo();
55
+ this.currentIndex--;
56
+ await this.invalidateQueries(command.modelName, command.constructor.name + 'Undo');
57
+ this.addNotification({ title: this.t('command.success.undo', 'Action annulée'), status: 'completed' });
58
+ } catch (error) {
59
+ console.error("Command undo failed:", error);
60
+ this.addNotification({ title: this.t('command.error.undo', 'Erreur d\'annulation'), message: error.message, status: 'error' });
61
+ }
62
+ }
63
+ }
64
+
65
+ async redo() {
66
+ if (this.canRedo()) {
67
+ try {
68
+ this.currentIndex++;
69
+ const command = this.history[this.currentIndex];
70
+ await command.execute();
71
+ await this.invalidateQueries(command.modelName, command.constructor.name);
72
+ this.addNotification({ title: this.t('command.success.redo', 'Action rétablie'), status: 'completed' });
73
+ } catch (error) {
74
+ console.error("Command redo failed:", error);
75
+ this.addNotification({ title: this.t('command.error.redo', 'Erreur de rétablissement'), message: error.message, status: 'error' });
76
+ }
77
+ }
78
+ }
79
+
80
+ canUndo() {
81
+ return this.currentIndex >= 0;
82
+ }
83
+
84
+ canRedo() {
85
+ return this.currentIndex < this.history.length - 1;
86
+ }
87
+
88
+ async invalidateQueries(modelName, commandType) {
89
+ // Solution plus fine : on invalide seulement les requêtes concernées.
90
+ // React Query s'occupera de rafraîchir les données de manière optimisée.
91
+ if (this.queryClient && modelName) {
92
+ // Invalide toutes les requêtes liées à ce modèle (listes, paginations, etc.)
93
+ await this.queryClient.invalidateQueries(['api/data', modelName]);
94
+ }
95
+ }
96
+ }
97
+
98
+ // --- Command Definitions ---
99
+
100
+ export class InsertCommand {
101
+ constructor(apiCall, modelName, formData) {
102
+ this.apiCall = apiCall;
103
+ this.modelName = modelName;
104
+ this.formData = { ...formData };
105
+ this.insertedItem = null; // On va stocker l'objet complet inséré
106
+ this.successMessage = "Donnée ajoutée";
107
+ }
108
+
109
+ async execute() {
110
+ // Si on a déjà un item (cas du redo), on le ré-insère. Sinon, on utilise le formData initial.
111
+ const dataToInsert = this.insertedItem ? { ...this.insertedItem, _id: undefined, _hash: undefined } : this.formData;
112
+
113
+ const response = await this.apiCall({ formData: dataToInsert, record: null });
114
+ if (!response.success) throw new Error(response.error || 'Insert failed');
115
+
116
+ // On stocke l'objet complet retourné par l'API, qui inclut le nouvel _id.
117
+ this.insertedItem = response.data;
118
+ if (!this.insertedItem?._id) throw new Error('No inserted data returned from API');
119
+ }
120
+
121
+ async undo() {
122
+ // On garde une copie de l'item avant de le supprimer pour le redo.
123
+ if (!this.insertedItem?._id) throw new Error("Cannot undo insert: item ID is missing.");
124
+ const response = await fetch(`/api/data/${this.insertedItem._id}`, { method: 'DELETE' });
125
+ const result = await response.json();
126
+ if (!response.ok || !result.success) {
127
+ throw new Error(result.error || 'Undo (delete) failed');
128
+ }
129
+ }
130
+ }
131
+
132
+ export class UpdateCommand {
133
+ constructor(apiCall, modelName, originalData, newData) {
134
+ this.apiCall = apiCall;
135
+ this.modelName = modelName;
136
+ this.originalData = { ...originalData }; // Copie pour l'undo
137
+ this.newData = { ...newData };
138
+ this.recordId = originalData._id;
139
+ this.successMessage = "Donnée mise à jour";
140
+ }
141
+
142
+ async execute() {
143
+ const response = await this.apiCall({ formData: this.newData, record: this.originalData });
144
+ if (!response.success) throw new Error(response.error || 'Update failed');
145
+ }
146
+
147
+ async undo() {
148
+ // Pour annuler, on ré-applique les données originales
149
+ await this.apiCall({ formData: this.originalData, record: this.originalData });
150
+ }
151
+ }
152
+
153
+ export class DeleteCommand {
154
+ constructor(apiCall, modelName, itemsToDelete) { // queryClient a été retiré
155
+ this.apiCall = apiCall;
156
+ this.modelName = modelName;
157
+ this.itemsToDelete = Array.isArray(itemsToDelete) ? [...itemsToDelete] : [itemsToDelete];
158
+ this.successMessage = "Donnée(s) supprimée(s)";
159
+ }
160
+
161
+ async execute() {
162
+ const response = await this.apiCall(this.itemsToDelete);
163
+ if (!response.success) throw new Error(response.error || 'Delete failed');
164
+ }
165
+
166
+ async undo() {
167
+ // Pour annuler, on ré-insère chaque document supprimé
168
+ // Note : cela crée de nouveaux _id, mais restaure les données.
169
+ const reinsertPromises = this.itemsToDelete.map(item => {
170
+ const reinsertData = { ...item };
171
+ delete reinsertData._id; // L'API doit générer un nouvel ID
172
+ delete reinsertData._hash;
173
+
174
+ return fetch('/api/data?_user=system', { // On ajoute _user=system pour tracer l'origine de l'action
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json' },
177
+ body: JSON.stringify({ model: this.modelName, data: reinsertData }),
178
+ }).then(res => res.json().then(data => {
179
+ if (!res.ok || !data.success) {
180
+ throw new Error(data.error || `Undo (re-insert) failed for item ${item._id}.`);
181
+ }
182
+ // On retourne l'objet complet retourné par l'API, qui contient le nouvel _id
183
+ return data.data;
184
+ }));
185
+ });
186
+
187
+ // On attend que toutes les réinsertions soient terminées.
188
+ const reinsertedItems = await Promise.all(reinsertPromises);
189
+
190
+ // *** CORRECTION CRUCIALE ***
191
+ // On met à jour la liste des items de la commande avec les nouvelles données (et les nouveaux _id).
192
+ // Ainsi, si un "redo" est exécuté, il ciblera les bons documents à supprimer.
193
+ this.itemsToDelete = reinsertedItems;
194
+ }
195
+ }
196
+
197
+ // --- Singleton Command Manager ---
198
+ // On crée l'instance du manager EN DEHORS du composant React.
199
+ // Ainsi, elle ne sera pas détruite et recréée à chaque re-rendu de l'application,
200
+ // ce qui permet de conserver l'historique des commandes (undo/redo).
201
+ const commandManagerInstance = new CommandManager();
202
+
203
+ // --- Provider Component ---
204
+ export const CommandProvider = ({ children, onResetQueryClient }) => {
205
+ const { addNotification } = useNotificationContext();
206
+ const { t, i18n } = useTranslation();
207
+ const queryClient = useQueryClient(); // On récupère le client ici
208
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
209
+
210
+ // On utilise l'instance unique et on met à jour ses dépendances via useEffect
211
+ // pour s'assurer qu'elle a toujours les dernières fonctions (qui sont recréées à chaque rendu).
212
+ const commandManagerRef = useRef(commandManagerInstance);
213
+ useEffect(() => {
214
+ commandManagerInstance.updateDependencies(addNotification, t, onResetQueryClient, queryClient);
215
+ }, [addNotification, t, onResetQueryClient, queryClient]);
216
+
217
+ const [canUndo, setCanUndo] = useState(commandManagerRef.current.canUndo());
218
+ const [canRedo, setCanRedo] = useState(commandManagerRef.current.canRedo());
219
+
220
+ const updateUndoRedoState = () => {
221
+ setCanUndo(commandManagerRef.current.canUndo());
222
+ setCanRedo(commandManagerRef.current.canRedo());
223
+ };
224
+
225
+ const execute = async (command) => {
226
+ await commandManagerRef.current.execute(command);
227
+ updateUndoRedoState();
228
+ };
229
+
230
+ const undo = async () => {
231
+ await commandManagerRef.current.undo();
232
+ updateUndoRedoState();
233
+ };
234
+
235
+ const redo = async () => {
236
+ await commandManagerRef.current.redo();
237
+ updateUndoRedoState();
238
+ };
239
+
240
+ // --- CORRECTION ---
241
+ // On inclut `setManagerContext` directement dans la valeur du contexte
242
+ // et on mémorise l'objet avec `useMemo` pour la stabilité.
243
+ const value = useMemo(() => ({
244
+ execute,
245
+ undo,
246
+ redo,
247
+ canUndo,
248
+ canRedo,
249
+ InsertCommand,
250
+ UpdateCommand,
251
+ DeleteCommand,
252
+ setManagerContext: (context) => {
253
+ commandManagerRef.current.context = context;
254
+ }
255
+ }), [canUndo, canRedo]); // Dépendances pour la mémorisation
256
+
257
+ return (
258
+ <CommandContext.Provider value={value}>{children}</CommandContext.Provider>
259
+ );
260
+ };
@@ -102,7 +102,10 @@ export const ModelProvider = ({ children }) => {
102
102
  params.append("_user", getUserId(me));
103
103
  params.append("depth", '1')
104
104
 
105
- let ids = [...new Set(rels.map(r => r._id || r))].join(',');
105
+ // SOLUTION : On trie les IDs pour garantir une queryKey stable,
106
+ // peu importe l'ordre dans lequel les relations ont été chargées.
107
+ let ids = [...new Set(rels.map(r => r._id || r))].sort().join(',');
108
+
106
109
  if (rels && rels.length > 0) {
107
110
  // Cas des relations : requête par IDs, sans pagination
108
111
  params.append('ids', ids);