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.
Files changed (63) hide show
  1. package/README.md +37 -0
  2. package/client/src/AddWidgetTypeModal.jsx +47 -43
  3. package/client/src/App.jsx +2 -6
  4. package/client/src/App.scss +13 -1
  5. package/client/src/AssistantChat.jsx +363 -323
  6. package/client/src/AssistantChat.scss +27 -10
  7. package/client/src/Dashboard.jsx +480 -396
  8. package/client/src/Dashboard.scss +1 -1
  9. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  10. package/client/src/DashboardView.jsx +654 -569
  11. package/client/src/DataEditor.jsx +10 -3
  12. package/client/src/DataLayout.jsx +807 -755
  13. package/client/src/DataLayout.scss +14 -0
  14. package/client/src/DataTable.jsx +39 -75
  15. package/client/src/Dialog.scss +1 -1
  16. package/client/src/Field.jsx +2057 -1825
  17. package/client/src/FlexViewCard.jsx +44 -0
  18. package/client/src/HistoryDialog.jsx +69 -14
  19. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  20. package/client/src/HtmlViewBuilderModal.scss +18 -0
  21. package/client/src/HtmlViewCard.jsx +44 -0
  22. package/client/src/HtmlViewCard.scss +35 -0
  23. package/client/src/KanbanCard.jsx +1 -2
  24. package/client/src/ModelCreator.jsx +5 -4
  25. package/client/src/ModelCreatorField.jsx +51 -4
  26. package/client/src/ModelList.jsx +280 -236
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/Notification.scss +0 -18
  29. package/client/src/Pagination.jsx +5 -3
  30. package/client/src/RelationField.jsx +354 -258
  31. package/client/src/RelationSelectorWidget.jsx +173 -0
  32. package/client/src/contexts/ModelContext.jsx +10 -1
  33. package/client/src/contexts/UIContext.jsx +72 -63
  34. package/client/src/filter.js +263 -212
  35. package/client/src/hooks/useValidation.js +75 -0
  36. package/client/src/translations.js +24 -24
  37. package/package.json +7 -6
  38. package/src/constants.js +1 -1
  39. package/src/core.js +8 -1
  40. package/src/defaultModels.js +1596 -1544
  41. package/src/engine.js +85 -43
  42. package/src/events.js +137 -113
  43. package/src/i18n.js +710 -10
  44. package/src/index.js +3 -0
  45. package/src/modules/assistant/assistant.js +253 -134
  46. package/src/modules/assistant/constants.js +2 -1
  47. package/src/modules/bucket.js +2 -1
  48. package/src/modules/data/data.core.js +118 -92
  49. package/src/modules/data/data.history.js +555 -492
  50. package/src/modules/data/data.js +3 -53
  51. package/src/modules/data/data.operations.js +3381 -3231
  52. package/src/modules/data/data.relations.js +686 -686
  53. package/src/modules/data/data.routes.js +1879 -1821
  54. package/src/modules/data/data.validation.js +81 -2
  55. package/src/modules/file.js +247 -238
  56. package/src/modules/user.js +1 -0
  57. package/src/modules/workflow.js +2 -2
  58. package/src/openai.jobs.js +3 -2
  59. package/src/packs.js +5482 -5478
  60. package/src/sso.js +2 -2
  61. package/src/workers/import-export-worker.js +1 -1
  62. package/test/data.history.integration.test.js +264 -192
  63. package/test/data.integration.test.js +149 -3
@@ -1,213 +1,264 @@
1
-
2
-
3
- export const convertInputValue = (value) => {
4
- if (typeof value !== 'string') return value;
5
-
6
- if (value.startsWith('$')) {
7
- // C'est une référence de champ
8
- return value;
9
- }
10
-
11
- // Si la valeur est une chaîne entourée de guillemets, on la conserve telle quelle
12
- if (/^['"].*['"]$/.test(value)) {
13
- return value.slice(1, -1); // Retire les guillemets
14
- }
15
-
16
- // Conversion en nombre si possible
17
- if (!isNaN(value) && value.trim() !== '') {
18
- return Number(value);
19
- }
20
-
21
- if (value.toLowerCase() === 'null') return null;
22
-
23
- // Conversion en booléen pour 'true' et 'false'
24
- if (value.toLowerCase() === 'true') return true;
25
- if (value.toLowerCase() === 'false') return false;
26
-
27
- // Par défaut, retourne la chaîne telle quelle
28
- return value;
29
- };
30
-
31
- const parseNode = (node, pathPrefix = '$') => {
32
- if (node && node.path && Array.isArray(node.path)) {
33
- return node;
34
- }
35
-
36
- if (!node || typeof node !== 'object' || Array.isArray(node)) {
37
- return null;
38
- }
39
-
40
- const keys = Object.keys(node);
41
- if (keys.length === 0) return null;
42
-
43
- // Gestion des $find
44
- if (keys.length === 1 && keys[0] === '$find') {
45
- const findCondition = node.$find;
46
- const parsedCondition = parseNode(findCondition, pathPrefix === '$' ? '$$this.' : pathPrefix);
47
-
48
- if (parsedCondition) {
49
- return {
50
- ...parsedCondition,
51
- path: parsedCondition.path || [],
52
- op: '$find',
53
- value: parsedCondition
54
- };
55
- }
56
- }
57
-
58
- // Gestion des opérateurs logiques
59
- if (keys.length === 1 && (keys[0] === '$and' || keys[0] === '$or')) {
60
- const children = node[keys[0]]
61
- .map(childNode => parseNode(childNode, pathPrefix))
62
- .filter(Boolean);
63
- return { [keys[0]]: children };
64
- }
65
-
66
- // Gestion des opérateurs de comparaison
67
- if (keys[0].startsWith('$')) {
68
- const op = keys[0];
69
- const args = node[op];
70
-
71
- // Cas spécial pour regexMatch
72
- if (op === '$regexMatch' && typeof args === 'object') {
73
- const transformExpr = reverseTransformExpr(args.input, pathPrefix === '$$this.');
74
- return {
75
- path: transformExpr.path || [args.input.substring(pathPrefix.length).split('.')],
76
- op: '$regex',
77
- value: args.regex,
78
- transform: transformExpr.transform
79
- };
80
- }
81
-
82
- // Cas spécial pour exists
83
- if ((op === '$ne' || op === '$eq') && Array.isArray(args) && args.length === 2 &&
84
- typeof args[0] === 'object' && args[0].$type && args[1] === 'missing') {
85
- const transformExpr = reverseTransformExpr(args[0].$type, pathPrefix === '$$this.');
86
- return {
87
- path: transformExpr.path || [args[0].$type.substring(pathPrefix.length).split('.')],
88
- op: '$exists',
89
- value: op === '$ne',
90
- transform: transformExpr.transform
91
- };
92
- }
93
-
94
- // Cas normal avec transformation
95
- if (Array.isArray(args) && args.length === 2) {
96
- const transformExpr = reverseTransformExpr(args[0], pathPrefix === '$$this.');
97
- return {
98
- path: transformExpr.path || [args[0].substring(pathPrefix.length).split('.')],
99
- op,
100
- value: args[1],
101
- transform: transformExpr.transform
102
- };
103
- }
104
-
105
- // Cas des transformations pures
106
- const transformExpr = reverseTransformExpr(node, pathPrefix === '$$this.');
107
- if (transformExpr && transformExpr.transform) {
108
- return {
109
- path: transformExpr.path || [keys[0].substring(1)],
110
- transform: transformExpr.transform
111
- };
112
- }
113
- }
114
-
115
- // Gestion des champs simples
116
- const path = keys[0];
117
- if (typeof path === 'string' && !path.startsWith('$')) {
118
- const condition = node[path];
119
- if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
120
- if (condition.$find) {
121
- const parsedInnerNode = parseNode(condition.$find, '$$this.');
122
- if (parsedInnerNode) {
123
- return {
124
- ...parsedInnerNode,
125
- path: [path, ...(parsedInnerNode.path || [])]
126
- };
127
- }
128
- } else {
129
- const parsedCondition = parseNode(condition, pathPrefix);
130
- if (parsedCondition) {
131
- return {
132
- ...parsedCondition,
133
- path: [path, ...(parsedCondition.path || [])]
134
- };
135
- }
136
- }
137
- } else {
138
- return {
139
- path: [path],
140
- op: '$eq',
141
- value: condition
142
- };
143
- }
144
- }
145
-
146
- return null;
147
- };
148
-
149
-
150
- export const buildRootFromExpr = (query) => {
151
- if (!query || typeof query !== 'object' || Array.isArray(query)) {
152
- return { $and: [] };
153
- }
154
-
155
- const exprToParse = query.$expr || query;
156
- const parsedRoot = parseNode(exprToParse);
157
-
158
- if (!parsedRoot) return { $and: [] };
159
- if (parsedRoot.$and || parsedRoot.$or) return parsedRoot;
160
-
161
- return { $and: [parsedRoot] };
162
- };
163
-
164
- export const pagedFilterToMongoConds = (pagedFilters, model) => {
165
- const modelFilter = pagedFilters?.[model.name] || {};
166
- const filterKeys = Object.keys(modelFilter);
167
-
168
- // Si le filtre est vide, on retourne un tableau vide.
169
- if (filterKeys.length === 0) {
170
- return [];
171
- }
172
-
173
- // Détecte si c'est un filtre avancé (contient des opérateurs logiques de haut niveau comme $and, $or, etc.)
174
- const isAdvancedFilter = filterKeys.some(key => key.startsWith('$'));
175
-
176
- if (isAdvancedFilter) {
177
- // C'est un filtre avancé du ConditionBuilder.
178
- // Il est déjà une condition MongoDB complète. On le met dans un tableau pour l'insérer dans le $and global.
179
- // Si le filtre avancé est vide (ex: { $and: [] }), on le retourne quand même pour que le ConditionBuilder puisse s'initialiser correctement.
180
- return [modelFilter];
181
- } else {
182
- // C'est un filtre simple (par colonne).
183
- // On transforme { champ1: cond1, champ2: cond2 } en [{ champ1: cond1 }, { champ2: cond2 }]
184
- const c = [];
185
- filterKeys.forEach(fieldName => {
186
- if (model.fields.some(f => f.name === fieldName)) {
187
- const condition = modelFilter[fieldName];
188
- if (condition && typeof condition === 'object' && Object.keys(condition).length > 0) {
189
- c.push(condition);
190
- }
191
- }
192
- });
193
- return c;
194
- }
195
- };
196
-
197
- /**
198
- * Remplace les placeholders dans un objet filtre.
199
- * Gère {{userId}}.
200
- */
201
- export const processFilterPlaceholders = (filter, user) => {
202
- const processedFilter = JSON.parse(JSON.stringify(filter));
203
- for (const key in processedFilter) {
204
- if (processedFilter[key] === '{{userId}}') {
205
- // Dans le système Primals, le champ utilisateur est souvent `_user`
206
- // et contient le nom d'utilisateur, pas l'ID. Adaptez si besoin.
207
- processedFilter[key] = user.username;
208
- } else if (typeof processedFilter[key] === 'object' && processedFilter[key] !== null) {
209
- processedFilter[key] = processFilterPlaceholders(processedFilter[key], user);
210
- }
211
- }
212
- return processedFilter;
1
+
2
+ /**
3
+ * Finds the matching {{/each}} for a {{#each ...}} tag at a given position.
4
+ * It correctly handles nested {{#each}} blocks.
5
+ * @param {string} template - The template string.
6
+ * @param {number} startIndex - The starting position of the opening {{#each ...}} tag.
7
+ * @returns {number} The position of the start of the matching {{/each}} tag, or -1 if not found.
8
+ */
9
+ function findMatchingEndEach(template, startIndex) {
10
+ const openTag = '{{#each';
11
+ const closeTag = '{{/each}}';
12
+ let level = 1;
13
+ let searchPos = startIndex + openTag.length;
14
+
15
+ while (level > 0 && searchPos < template.length) {
16
+ const nextClose = template.indexOf(closeTag, searchPos);
17
+ if (nextClose === -1) {
18
+ return -1; // Unmatched opening tag
19
+ }
20
+
21
+ const nextOpen = template.indexOf(openTag, searchPos);
22
+
23
+ if (nextOpen !== -1 && nextOpen < nextClose) {
24
+ level++;
25
+ searchPos = nextOpen + openTag.length;
26
+ } else {
27
+ level--;
28
+ searchPos = nextClose + closeTag.length;
29
+ if (level === 0) {
30
+ return nextClose; // Found the matching closing tag
31
+ }
32
+ }
33
+ }
34
+ return -1; // No matching closing tag found
35
+ }
36
+
37
+ /**
38
+ * Renders a simple HTML template by substituting placeholders.
39
+ * This is a robust version that correctly handles nested {{#each}} loops.
40
+ *
41
+ * @param {string} templateString The template with placeholders.
42
+ * @param {Array<object>|object} data The data to inject.
43
+ * @returns {string} The rendered HTML string.
44
+ */
45
+
46
+ /**
47
+ * Renders a simple HTML template by substituting placeholders.
48
+ * This is a robust version that correctly handles nested {{#each}} loops.
49
+ *
50
+ * @param {string} templateString The template with placeholders.
51
+ * @param {Array<object>|object} data The data to inject.
52
+ * @returns {string} The rendered HTML string.
53
+ */
54
+ export const convertInputValue = (value) => {
55
+ if (typeof value !== 'string') return value;
56
+
57
+ if (value.startsWith('$')) {
58
+ // C'est une référence de champ
59
+ return value;
60
+ }
61
+
62
+ // Si la valeur est une chaîne entourée de guillemets, on la conserve telle quelle
63
+ if (/^['"].*['"]$/.test(value)) {
64
+ return value.slice(1, -1); // Retire les guillemets
65
+ }
66
+
67
+ // Conversion en nombre si possible
68
+ if (!isNaN(value) && value.trim() !== '') {
69
+ return Number(value);
70
+ }
71
+
72
+ if (value.toLowerCase() === 'null') return null;
73
+
74
+ // Conversion en booléen pour 'true' et 'false'
75
+ if (value.toLowerCase() === 'true') return true;
76
+ if (value.toLowerCase() === 'false') return false;
77
+
78
+ // Par défaut, retourne la chaîne telle quelle
79
+ return value;
80
+ };
81
+
82
+ const parseNode = (node, pathPrefix = '$') => {
83
+ if (node && node.path && Array.isArray(node.path)) {
84
+ return node;
85
+ }
86
+
87
+ if (!node || typeof node !== 'object' || Array.isArray(node)) {
88
+ return null;
89
+ }
90
+
91
+ const keys = Object.keys(node);
92
+ if (keys.length === 0) return null;
93
+
94
+ // Gestion des $find
95
+ if (keys.length === 1 && keys[0] === '$find') {
96
+ const findCondition = node.$find;
97
+ const parsedCondition = parseNode(findCondition, pathPrefix === '$' ? '$$this.' : pathPrefix);
98
+
99
+ if (parsedCondition) {
100
+ return {
101
+ ...parsedCondition,
102
+ path: parsedCondition.path || [],
103
+ op: '$find',
104
+ value: parsedCondition
105
+ };
106
+ }
107
+ }
108
+
109
+ // Gestion des opérateurs logiques
110
+ if (keys.length === 1 && (keys[0] === '$and' || keys[0] === '$or')) {
111
+ const children = node[keys[0]]
112
+ .map(childNode => parseNode(childNode, pathPrefix))
113
+ .filter(Boolean);
114
+ return { [keys[0]]: children };
115
+ }
116
+
117
+ // Gestion des opérateurs de comparaison
118
+ if (keys[0].startsWith('$')) {
119
+ const op = keys[0];
120
+ const args = node[op];
121
+
122
+ // Cas spécial pour regexMatch
123
+ if (op === '$regexMatch' && typeof args === 'object') {
124
+ const transformExpr = reverseTransformExpr(args.input, pathPrefix === '$$this.');
125
+ return {
126
+ path: transformExpr.path || [args.input.substring(pathPrefix.length).split('.')],
127
+ op: '$regex',
128
+ value: args.regex,
129
+ transform: transformExpr.transform
130
+ };
131
+ }
132
+
133
+ // Cas spécial pour exists
134
+ if ((op === '$ne' || op === '$eq') && Array.isArray(args) && args.length === 2 &&
135
+ typeof args[0] === 'object' && args[0].$type && args[1] === 'missing') {
136
+ const transformExpr = reverseTransformExpr(args[0].$type, pathPrefix === '$$this.');
137
+ return {
138
+ path: transformExpr.path || [args[0].$type.substring(pathPrefix.length).split('.')],
139
+ op: '$exists',
140
+ value: op === '$ne',
141
+ transform: transformExpr.transform
142
+ };
143
+ }
144
+
145
+ // Cas normal avec transformation
146
+ if (Array.isArray(args) && args.length === 2) {
147
+ const transformExpr = reverseTransformExpr(args[0], pathPrefix === '$$this.');
148
+ return {
149
+ path: transformExpr.path || [args[0].substring(pathPrefix.length).split('.')],
150
+ op,
151
+ value: args[1],
152
+ transform: transformExpr.transform
153
+ };
154
+ }
155
+
156
+ // Cas des transformations pures
157
+ const transformExpr = reverseTransformExpr(node, pathPrefix === '$$this.');
158
+ if (transformExpr && transformExpr.transform) {
159
+ return {
160
+ path: transformExpr.path || [keys[0].substring(1)],
161
+ transform: transformExpr.transform
162
+ };
163
+ }
164
+ }
165
+
166
+ // Gestion des champs simples
167
+ const path = keys[0];
168
+ if (typeof path === 'string' && !path.startsWith('$')) {
169
+ const condition = node[path];
170
+ if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
171
+ if (condition.$find) {
172
+ const parsedInnerNode = parseNode(condition.$find, '$$this.');
173
+ if (parsedInnerNode) {
174
+ return {
175
+ ...parsedInnerNode,
176
+ path: [path, ...(parsedInnerNode.path || [])]
177
+ };
178
+ }
179
+ } else {
180
+ const parsedCondition = parseNode(condition, pathPrefix);
181
+ if (parsedCondition) {
182
+ return {
183
+ ...parsedCondition,
184
+ path: [path, ...(parsedCondition.path || [])]
185
+ };
186
+ }
187
+ }
188
+ } else {
189
+ return {
190
+ path: [path],
191
+ op: '$eq',
192
+ value: condition
193
+ };
194
+ }
195
+ }
196
+
197
+ return null;
198
+ };
199
+
200
+
201
+ export const buildRootFromExpr = (query) => {
202
+ if (!query || typeof query !== 'object' || Array.isArray(query)) {
203
+ return { $and: [] };
204
+ }
205
+
206
+ const exprToParse = query.$expr || query;
207
+ const parsedRoot = parseNode(exprToParse);
208
+
209
+ if (!parsedRoot) return { $and: [] };
210
+ if (parsedRoot.$and || parsedRoot.$or) return parsedRoot;
211
+
212
+ return { $and: [parsedRoot] };
213
+ };
214
+
215
+ export const pagedFilterToMongoConds = (pagedFilters, model) => {
216
+ const modelFilter = pagedFilters?.[model.name] || {};
217
+ const filterKeys = Object.keys(modelFilter);
218
+
219
+ // Si le filtre est vide, on retourne un tableau vide.
220
+ if (filterKeys.length === 0) {
221
+ return [];
222
+ }
223
+
224
+ // Détecte si c'est un filtre avancé (contient des opérateurs logiques de haut niveau comme $and, $or, etc.)
225
+ const isAdvancedFilter = filterKeys.some(key => key.startsWith('$'));
226
+
227
+ if (isAdvancedFilter) {
228
+ // C'est un filtre avancé du ConditionBuilder.
229
+ // Il est déjà une condition MongoDB complète. On le met dans un tableau pour l'insérer dans le $and global.
230
+ // Si le filtre avancé est vide (ex: { $and: [] }), on le retourne quand même pour que le ConditionBuilder puisse s'initialiser correctement.
231
+ return [modelFilter];
232
+ } else {
233
+ // C'est un filtre simple (par colonne).
234
+ // On transforme { champ1: cond1, champ2: cond2 } en [{ champ1: cond1 }, { champ2: cond2 }]
235
+ const c = [];
236
+ filterKeys.forEach(fieldName => {
237
+ if (model.fields.some(f => f.name === fieldName)) {
238
+ const condition = modelFilter[fieldName];
239
+ if (condition && typeof condition === 'object' && Object.keys(condition).length > 0) {
240
+ c.push(condition);
241
+ }
242
+ }
243
+ });
244
+ return c;
245
+ }
246
+ };
247
+
248
+ /**
249
+ * Remplace les placeholders dans un objet filtre.
250
+ * Gère {{userId}}.
251
+ */
252
+ export const processFilterPlaceholders = (filter, user) => {
253
+ const processedFilter = JSON.parse(JSON.stringify(filter));
254
+ for (const key in processedFilter) {
255
+ if (processedFilter[key] === '{{userId}}') {
256
+ // Dans le système Primals, le champ utilisateur est souvent `_user`
257
+ // et contient le nom d'utilisateur, pas l'ID. Adaptez si besoin.
258
+ processedFilter[key] = user.username;
259
+ } else if (typeof processedFilter[key] === 'object' && processedFilter[key] !== null) {
260
+ processedFilter[key] = processFilterPlaceholders(processedFilter[key], user);
261
+ }
262
+ }
263
+ return processedFilter;
213
264
  };
@@ -0,0 +1,75 @@
1
+ import {useMutation, useQuery} from 'react-query';
2
+ import { debounce } from '../../../src/core.js';
3
+ import { useState, useCallback, useMemo } from 'react';
4
+
5
+ /**
6
+ * Validates a field on the server.
7
+ * @param {object} api - The API client instance.
8
+ * @param {object} payload - The validation payload.
9
+ * @param {string} payload.model - The model name.
10
+ * @param {object} payload.data - The field and value to validate, e.g., { email: '...' }.
11
+ * @param {string} [payload.contextId] - The ID of the document being edited.
12
+ * @returns {Promise<any>}
13
+ */
14
+ const validateFieldOnServer = async (api, payload) => {
15
+ return fetch('/api/data/validate', { method: 'POST',
16
+ headers: {
17
+ 'Content-Type': 'application/json'
18
+ }
19
+ });
20
+ };
21
+
22
+ export const useRealtimeValidation = (modelName, docId) => {
23
+
24
+ const [validationState, setValidationState] = useState({}); // { fieldName: { status, error } }
25
+
26
+ const mutation = useMutation(
27
+ (payload) => validateFieldOnServer(api, payload),
28
+ {
29
+ onSuccess: (data, variables) => {
30
+ const fieldName = Object.keys(variables.data)[0];
31
+ setValidationState(prev => ({
32
+ ...prev,
33
+ [fieldName]: { status: 'valid', error: null }
34
+ }));
35
+ },
36
+ onError: (error, variables) => {
37
+ const fieldName = Object.keys(variables.data)[0];
38
+ setValidationState(prev => ({
39
+ ...prev,
40
+ [fieldName]: { status: 'invalid', error: error.response?.data?.error || 'Validation failed' }
41
+ }));
42
+ }
43
+ }
44
+ );
45
+
46
+ const debouncedValidate = useMemo(
47
+ () => debounce((field, value) => {
48
+ if (value === '' || value === null || value === undefined) {
49
+ setValidationState(prev => ({ ...prev, [field]: { status: 'idle', error: null } }));
50
+ mutation.reset();
51
+ return;
52
+ }
53
+
54
+ setValidationState(prev => ({ ...prev, [field]: { status: 'validating', error: null } }));
55
+ mutation.mutate({
56
+ model: modelName,
57
+ contextId: docId,
58
+ data: { [field]: value }
59
+ });
60
+ }, 500),
61
+ [modelName, docId, mutation]
62
+ );
63
+
64
+ const validate = useCallback((field, value) => {
65
+ if (!modelName) return; // Don't validate if not configured
66
+ debouncedValidate(field, value);
67
+ }, [debouncedValidate, modelName]);
68
+
69
+ const resetValidation = useCallback(() => {
70
+ setValidationState({});
71
+ mutation.reset();
72
+ }, [mutation]);
73
+
74
+ return { validate, validationState, resetValidation };
75
+ };