data-primals-engine 1.5.0 → 1.5.2
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/README.md +37 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +2 -6
- package/client/src/App.scss +13 -1
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +27 -10
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +654 -569
- package/client/src/DataEditor.jsx +10 -3
- package/client/src/DataLayout.jsx +807 -755
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +39 -75
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +2057 -1825
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +69 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +5 -4
- package/client/src/ModelCreatorField.jsx +51 -4
- package/client/src/ModelList.jsx +280 -236
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +263 -212
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +24 -24
- package/package.json +7 -6
- package/src/constants.js +1 -1
- package/src/core.js +8 -1
- package/src/defaultModels.js +1596 -1544
- package/src/engine.js +85 -43
- package/src/events.js +137 -113
- package/src/i18n.js +710 -10
- package/src/index.js +3 -0
- package/src/modules/assistant/assistant.js +253 -134
- package/src/modules/assistant/constants.js +2 -1
- package/src/modules/bucket.js +2 -1
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +555 -492
- package/src/modules/data/data.js +3 -53
- package/src/modules/data/data.operations.js +3381 -3231
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1821
- package/src/modules/data/data.validation.js +81 -2
- package/src/modules/file.js +247 -238
- package/src/modules/user.js +1 -0
- package/src/modules/workflow.js +2 -2
- package/src/openai.jobs.js +3 -2
- package/src/packs.js +5482 -5478
- package/src/sso.js +2 -2
- package/src/workers/import-export-worker.js +1 -1
- package/test/data.history.integration.test.js +264 -192
- package/test/data.integration.test.js +149 -3
|
@@ -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;
|
|
@@ -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 [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
};
|