data-primals-engine 1.4.3 → 1.5.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 (77) hide show
  1. package/README.md +913 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/AddWidgetTypeModal.jsx +47 -43
  5. package/client/src/App.jsx +3 -7
  6. package/client/src/App.scss +25 -3
  7. package/client/src/AssistantChat.jsx +363 -323
  8. package/client/src/AssistantChat.scss +30 -12
  9. package/client/src/Dashboard.jsx +480 -396
  10. package/client/src/Dashboard.scss +1 -1
  11. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  12. package/client/src/DashboardView.jsx +104 -19
  13. package/client/src/DataEditor.jsx +12 -5
  14. package/client/src/DataLayout.jsx +805 -762
  15. package/client/src/DataLayout.scss +14 -0
  16. package/client/src/DataTable.jsx +63 -77
  17. package/client/src/Dialog.scss +1 -1
  18. package/client/src/Field.jsx +591 -322
  19. package/client/src/FlexDataRenderer.jsx +2 -0
  20. package/client/src/FlexTreeUtils.js +1 -1
  21. package/client/src/FlexViewCard.jsx +44 -0
  22. package/client/src/HistoryDialog.jsx +47 -14
  23. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  24. package/client/src/HtmlViewBuilderModal.scss +18 -0
  25. package/client/src/HtmlViewCard.jsx +44 -0
  26. package/client/src/HtmlViewCard.scss +35 -0
  27. package/client/src/KPIDialog.jsx +11 -1
  28. package/client/src/KanbanCard.jsx +1 -2
  29. package/client/src/ModelCreator.jsx +6 -6
  30. package/client/src/ModelCreatorField.jsx +74 -31
  31. package/client/src/ModelList.jsx +93 -54
  32. package/client/src/Notification.jsx +136 -136
  33. package/client/src/Notification.scss +0 -18
  34. package/client/src/Pagination.jsx +5 -3
  35. package/client/src/RelationField.jsx +354 -258
  36. package/client/src/RelationSelectorWidget.jsx +173 -0
  37. package/client/src/constants.js +1 -1
  38. package/client/src/contexts/ModelContext.jsx +10 -1
  39. package/client/src/contexts/UIContext.jsx +72 -63
  40. package/client/src/filter.js +262 -212
  41. package/client/src/hooks/useTutorials.jsx +62 -65
  42. package/client/src/hooks/useValidation.js +75 -0
  43. package/client/src/translations.js +26 -24
  44. package/package.json +3 -1
  45. package/perf/README.md +147 -0
  46. package/perf/artillery-hooks.js +37 -0
  47. package/perf/perf-shot-hardwork.yml +84 -0
  48. package/perf/perf-shot-search.yml +45 -0
  49. package/perf/setup.yml +26 -0
  50. package/server.js +1 -1
  51. package/src/constants.js +3 -28
  52. package/src/core.js +15 -1
  53. package/src/data.js +1 -1
  54. package/src/defaultModels.js +63 -7
  55. package/src/email.js +5 -2
  56. package/src/engine.js +5 -3
  57. package/src/filter.js +5 -3
  58. package/src/i18n.js +710 -10
  59. package/src/modules/assistant/assistant.js +151 -19
  60. package/src/modules/bucket.js +14 -16
  61. package/src/modules/data/data.backup.js +11 -8
  62. package/src/modules/data/data.core.js +118 -92
  63. package/src/modules/data/data.history.js +531 -492
  64. package/src/modules/data/data.js +9 -56
  65. package/src/modules/data/data.operations.js +3282 -2999
  66. package/src/modules/data/data.relations.js +686 -686
  67. package/src/modules/data/data.routes.js +118 -24
  68. package/src/modules/data/data.scheduling.js +2 -1
  69. package/src/modules/data/data.validation.js +85 -3
  70. package/src/modules/file.js +247 -236
  71. package/src/modules/user.js +4 -1
  72. package/src/modules/workflow.js +9 -10
  73. package/src/openai.jobs.js +2 -0
  74. package/src/packs.js +5482 -5461
  75. package/src/providers.js +22 -7
  76. package/test/data.integration.test.js +136 -2
  77. package/test/import_export.integration.test.js +1 -1
@@ -0,0 +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
+
173
+ export default RelationSelectorWidget;
@@ -57,7 +57,7 @@ export const profiles = {
57
57
  }, // budget,
58
58
  'developer': {
59
59
  "packs": ["Multilingual starter pack", "Website Starter Pack"],
60
- "models": ['alert','endpoint','request','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role']
60
+ "models": ['alert','endpoint','request','kpi','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role']
61
61
  },
62
62
  'company': {
63
63
  "packs": ["Multilingual starter pack", "E-commerce Starter Kit"],
@@ -77,6 +77,15 @@ export const ModelProvider = ({ children }) => {
77
77
  }
78
78
  }, [selectedModel]);
79
79
 
80
+ // This effect synchronizes the selected model with the list of models to load paginated data for.
81
+ // It's crucial for isolated components like RelationSelectorWidget that use their own ModelProvider.
82
+ // When a model is selected in such a widget, this ensures the DataTable inside it will fetch and display data.
83
+ useEffect(() => {
84
+ if (selectedModel?.name) {
85
+ setFilteredDatasToLoad([selectedModel.name]);
86
+ }
87
+ }, [selectedModel]);
88
+
80
89
  let abortController = new AbortController();
81
90
 
82
91
  const [relations, setRelations] = useState([]);
@@ -207,7 +216,7 @@ export const ModelProvider = ({ children }) => {
207
216
  enabled: !!selectedModel,
208
217
  onSettled: (data)=>{
209
218
  try {
210
- Object.keys(onSuccessCallbacks?.['api/data/' + model.name + '/paged']).forEach(cb => {
219
+ Object.keys(onSuccessCallbacks?.['api/data/' + model.name + '/paged'] || []).forEach(cb => {
211
220
  onSuccessCallbacks?.['api/data/' + model.name + '/paged'][cb](data)
212
221
  });
213
222
  } catch (e){
@@ -1,64 +1,73 @@
1
- // ModelContext.jsx
2
- import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
3
- import useLocalStorage from "../hooks/useLocalStorage.js";
4
-
5
- const UIContext = createContext(null);
6
-
7
- export const UIProvider = ({ children }) => {
8
-
9
- const [tourStepIndex, setTourStepIndex] = useState(0);
10
- const [currentTourSteps, setCurrentTourSteps] = useState([]);
11
- const [isTourOpen, setIsTourOpen] = useState(false);
12
- const [allTourSteps, setAllTourSteps] = useState({});
13
-
14
- const [chartToAdd, setChartToAdd] = useState(null);
15
-
16
- const [currentTour, setCurrentTour] = useLocalStorage("spotlight-tour", null);
17
- // This is the single source of truth for tours that have been launched.
18
- // It correctly reads from localStorage on initial load and persists any changes.
19
- const [launchedTours, setLaunchedTours] = useLocalStorage('launchedTours', []);
20
-
21
- // A stable helper function to add a tour to the list without overwriting.
22
- const addLaunchedTour = useCallback((tourName) => {
23
- setLaunchedTours(prevTours => {
24
- // Ensure we're working with an array, even if localStorage is empty/corrupted.
25
- const currentTours = Array.isArray(prevTours) ? prevTours : [];
26
- if (!currentTours.includes(tourName)) {
27
- return [...currentTours, tourName];
28
- }
29
- return currentTours; // Return unchanged if already present
30
- });
31
- }, [setLaunchedTours]); // setLaunchedTours from useLocalStorage is stable
32
-
33
- const [isLocked, setLocked] = useState(false);
34
- const contextValue = useMemo(() => ({
35
- isLocked,
36
- setLocked,
37
- currentTourSteps, setCurrentTourSteps,
38
- launchedTours, setLaunchedTours, addLaunchedTour,
39
- currentTour, setCurrentTour,
40
- isTourOpen, setIsTourOpen, setAllTourSteps, allTourSteps,
41
- tourStepIndex, setTourStepIndex, chartToAdd, setChartToAdd
42
- }), [isLocked,
43
- setLocked,
44
- currentTourSteps, setCurrentTourSteps,
45
- launchedTours,setLaunchedTours,
46
- currentTour, setCurrentTour,
47
- isTourOpen, setIsTourOpen, setAllTourSteps, allTourSteps,
48
- tourStepIndex, setTourStepIndex, addLaunchedTour,
49
- chartToAdd, setChartToAdd]);
50
-
51
- return (
52
- <UIContext.Provider value={contextValue}>
53
- <div id={"ui"} className={`${isLocked ? 'ui-locked' : ''}`}>{children}</div>
54
- </UIContext.Provider>
55
- );
56
- };
57
-
58
- export const useUI = () => {
59
- const context = useContext(UIContext);
60
- if (context === null) {
61
- throw new Error('useUI doit être utilisé dans un UIProvider');
62
- }
63
- return context;
1
+ // ModelContext.jsx
2
+ import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
3
+ import useLocalStorage from "../hooks/useLocalStorage.js";
4
+
5
+ const UIContext = createContext(null);
6
+
7
+ export const UIProvider = ({ children }) => {
8
+
9
+ const [tourStepIndex, setTourStepIndex] = useState(0);
10
+ const [currentTourSteps, setCurrentTourSteps] = useState([]);
11
+ const [isTourOpen, setIsTourOpen] = useState(false);
12
+ const [allTourSteps, setAllTourSteps] = useState({});
13
+
14
+ const [chartToAdd, setChartToAdd] = useState(null);
15
+ const [flexViewToAdd, setFlexViewToAdd] = useState(null);
16
+ const [htmlViewToAdd, setHtmlViewToAdd] = useState(null);
17
+
18
+ const [currentTour, setCurrentTour] = useLocalStorage("spotlight-tour", null);
19
+ // This is the single source of truth for tours that have been launched.
20
+ // It correctly reads from localStorage on initial load and persists any changes.
21
+ const [launchedTours, setLaunchedTours] = useLocalStorage('launchedTours', []);
22
+
23
+ // A stable helper function to add a tour to the list without overwriting.
24
+ const addLaunchedTour = useCallback((tourName) => {
25
+ setLaunchedTours(prevTours => {
26
+ // Ensure we're working with an array, even if localStorage is empty/corrupted.
27
+ const currentTours = Array.isArray(prevTours) ? prevTours : [];
28
+ if (!currentTours.includes(tourName)) {
29
+ return [...currentTours, tourName];
30
+ }
31
+ return currentTours; // Return unchanged if already present
32
+ });
33
+ }, [setLaunchedTours]); // setLaunchedTours from useLocalStorage is stable
34
+
35
+ const [assistantConfig, setAssistantConfig] = useState(null);
36
+ const [isLocked, setLocked] = useState(false);
37
+ const contextValue = useMemo(() => ({
38
+ isLocked,
39
+ setLocked,
40
+ currentTourSteps, setCurrentTourSteps,
41
+ launchedTours, setLaunchedTours, addLaunchedTour,
42
+ currentTour, setCurrentTour,
43
+ isTourOpen, setIsTourOpen, setAllTourSteps, allTourSteps,
44
+ tourStepIndex, setTourStepIndex,
45
+ chartToAdd, setChartToAdd,
46
+ flexViewToAdd, setFlexViewToAdd,
47
+ htmlViewToAdd, setHtmlViewToAdd,
48
+ assistantConfig, setAssistantConfig
49
+ }), [isLocked,
50
+ setLocked,
51
+ currentTourSteps, setCurrentTourSteps,
52
+ launchedTours,setLaunchedTours,
53
+ currentTour, setCurrentTour,
54
+ isTourOpen, setIsTourOpen, setAllTourSteps, allTourSteps,
55
+ tourStepIndex, setTourStepIndex, addLaunchedTour, chartToAdd, setChartToAdd,
56
+ flexViewToAdd, setFlexViewToAdd, htmlViewToAdd, setHtmlViewToAdd,
57
+ assistantConfig, setAssistantConfig
58
+ ]);
59
+
60
+ return (
61
+ <UIContext.Provider value={contextValue}>
62
+ <div id={"ui"} className={`${isLocked ? 'ui-locked' : ''}`}>{children}</div>
63
+ </UIContext.Provider>
64
+ );
65
+ };
66
+
67
+ export const useUI = () => {
68
+ const context = useContext(UIContext);
69
+ if (context === null) {
70
+ throw new Error('useUI doit être utilisé dans un UIProvider');
71
+ }
72
+ return context;
64
73
  };