data-primals-engine 1.0.10 → 1.1.0

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 (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +5390 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. package/vitest.config.js +2 -2
@@ -0,0 +1,222 @@
1
+
2
+ export const MONGO_CALC_OPERATORS = {
3
+ $find: {
4
+ label: 'Find in relation',
5
+ description: 'Recherche dans une relation/tableau',
6
+ supportsNesting: true,
7
+ multi: false,
8
+ isFindOperator: true // Nouvelle propriété pour identifier les opérateurs $find
9
+ },
10
+ $and: { label: 'Et (and)', description: 'Renvoie vrai si toutes les conditions sont vérifiées', args: 1, multi: true },
11
+ $or: { label: 'Ou (or)', description: 'Renvoie vrai si au moins une des conditions est vérifiée.', args: 1, multi: true },
12
+ $not: { label: 'Non (not)', description: 'Inverse la condition (ex: non égal à).', args: 1, multi: true },
13
+ $nor: { label: 'Ou Non (nor)', description: 'Renvoie vrai si aucune des conditions n\'est vérifiée', args: 1, multi: true },
14
+ $eq: { label: '=', multi: false, args: 2 },
15
+ $ne: { label: '!=', multi: false, args: 2 },
16
+ $gt: { label: '>', multi: false, args: 2 },
17
+ $gte: { label: '>=', multi: false, args: 2 },
18
+ $lt: { label: '<', multi: false, args: 2 },
19
+ $lte: { label: '<=', multi: false, args: 2 },
20
+ $in: { label: 'in', multi: true, args: 2 },
21
+ $nin: { label: 'nin', multi: true, args: 2 },
22
+ $all: { label: 'afll', multi: true },
23
+ $size: {
24
+ label: 'size',
25
+ multi: false,
26
+ description: 'Renvoie le nombre d\'éléments dans un tableau',
27
+ disableAdvancedValue: true
28
+ },
29
+ $elemMatch: { label: 'elemMatch', multi: true },
30
+ $type: {
31
+ label: 'type',
32
+ description: 'Renvoie le type BSON de l\'élément',
33
+ multi: false,
34
+ disableAdvancedValue: true
35
+ },
36
+ $add: { label: '+', multi: true },
37
+ $subtract: { label: '-', multi: false, args: 2 },
38
+ $multiply: { label: '*', multi: true },
39
+ $divide: { label: '/', multi: false, args: 2 },
40
+ $mod: { label: '%', multi: false, args: 2 },
41
+ $pow: { label: 'pow', multi: false },
42
+ $sqrt: { label: 'sqrt', multi: false },
43
+ $exp: { label: 'exp', multi: false },
44
+ $abs: { label: 'abs', multi: false },
45
+ $ceil: { label: 'ceil', multi: false },
46
+ $floor: { label: 'floor', multi: false },
47
+ $ln: { label: 'ln', multi: false },
48
+ $log10: { label: 'log', multi: false },
49
+ $concat: { label: 'concat', multi: true },
50
+ $year: { label: 'year', multi: false, isDate: true },
51
+ $month: { label: 'month', multi: false, isDate: true },
52
+ $dayOfMonth: { label: 'dayOfMonth', multi: false, isDate: true },
53
+ $hour: { label: 'hour', multi: false, isDate: true },
54
+ $minute: { label: 'minute', multi: false, isDate: true },
55
+ $second: { label: 'second', multi: false, isDate: true },
56
+ $millisecond: { label: 'ms', multi: false, isDate: true },
57
+ $toBool: { label: 'toBool', multi: false, converter: true },
58
+ $toString: { label: 'toString', multi: false, converter: true },
59
+ $toInt: { label: 'toInt', multi: false, converter: true },
60
+ $toDouble: { label: 'toDouble', multi: false, converter: true },
61
+ };
62
+
63
+ export const convertInputValue = (value) => {
64
+ if (typeof value !== 'string') return value;
65
+
66
+ if (value.startsWith('$')) {
67
+ // C'est une référence de champ
68
+ return value;
69
+ }
70
+
71
+ // Si la valeur est une chaîne entourée de guillemets, on la conserve telle quelle
72
+ if (/^['"].*['"]$/.test(value)) {
73
+ return value.slice(1, -1); // Retire les guillemets
74
+ }
75
+
76
+ // Conversion en nombre si possible
77
+ if (!isNaN(value) && value.trim() !== '') {
78
+ return Number(value);
79
+ }
80
+
81
+ if (value.toLowerCase() === 'null') return null;
82
+
83
+ // Conversion en booléen pour 'true' et 'false'
84
+ if (value.toLowerCase() === 'true') return true;
85
+ if (value.toLowerCase() === 'false') return false;
86
+
87
+ // Par défaut, retourne la chaîne telle quelle
88
+ return value;
89
+ };
90
+
91
+ const parseNode = (node, pathPrefix = '$') => {
92
+ if (node && node.path && Array.isArray(node.path)) {
93
+ return node;
94
+ }
95
+
96
+ if (!node || typeof node !== 'object' || Array.isArray(node)) {
97
+ return null;
98
+ }
99
+
100
+ const keys = Object.keys(node);
101
+ if (keys.length === 0) return null;
102
+
103
+ // Gestion des $find
104
+ if (keys.length === 1 && keys[0] === '$find') {
105
+ const findCondition = node.$find;
106
+ const parsedCondition = parseNode(findCondition, pathPrefix === '$' ? '$$this.' : pathPrefix);
107
+
108
+ if (parsedCondition) {
109
+ return {
110
+ ...parsedCondition,
111
+ path: parsedCondition.path || [],
112
+ op: '$find',
113
+ value: parsedCondition
114
+ };
115
+ }
116
+ }
117
+
118
+ // Gestion des opérateurs logiques
119
+ if (keys.length === 1 && (keys[0] === '$and' || keys[0] === '$or')) {
120
+ const children = node[keys[0]]
121
+ .map(childNode => parseNode(childNode, pathPrefix))
122
+ .filter(Boolean);
123
+ return { [keys[0]]: children };
124
+ }
125
+
126
+ // Gestion des opérateurs de comparaison
127
+ if (keys[0].startsWith('$')) {
128
+ const op = keys[0];
129
+ const args = node[op];
130
+
131
+ // Cas spécial pour regexMatch
132
+ if (op === '$regexMatch' && typeof args === 'object') {
133
+ const transformExpr = reverseTransformExpr(args.input, pathPrefix === '$$this.');
134
+ return {
135
+ path: transformExpr.path || [args.input.substring(pathPrefix.length).split('.')],
136
+ op: '$regex',
137
+ value: args.regex,
138
+ transform: transformExpr.transform
139
+ };
140
+ }
141
+
142
+ // Cas spécial pour exists
143
+ if ((op === '$ne' || op === '$eq') && Array.isArray(args) && args.length === 2 &&
144
+ typeof args[0] === 'object' && args[0].$type && args[1] === 'missing') {
145
+ const transformExpr = reverseTransformExpr(args[0].$type, pathPrefix === '$$this.');
146
+ return {
147
+ path: transformExpr.path || [args[0].$type.substring(pathPrefix.length).split('.')],
148
+ op: '$exists',
149
+ value: op === '$ne',
150
+ transform: transformExpr.transform
151
+ };
152
+ }
153
+
154
+ // Cas normal avec transformation
155
+ if (Array.isArray(args) && args.length === 2) {
156
+ const transformExpr = reverseTransformExpr(args[0], pathPrefix === '$$this.');
157
+ return {
158
+ path: transformExpr.path || [args[0].substring(pathPrefix.length).split('.')],
159
+ op,
160
+ value: args[1],
161
+ transform: transformExpr.transform
162
+ };
163
+ }
164
+
165
+ // Cas des transformations pures
166
+ const transformExpr = reverseTransformExpr(node, pathPrefix === '$$this.');
167
+ if (transformExpr && transformExpr.transform) {
168
+ return {
169
+ path: transformExpr.path || [keys[0].substring(1)],
170
+ transform: transformExpr.transform
171
+ };
172
+ }
173
+ }
174
+
175
+ // Gestion des champs simples
176
+ const path = keys[0];
177
+ if (typeof path === 'string' && !path.startsWith('$')) {
178
+ const condition = node[path];
179
+ if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
180
+ if (condition.$find) {
181
+ const parsedInnerNode = parseNode(condition.$find, '$$this.');
182
+ if (parsedInnerNode) {
183
+ return {
184
+ ...parsedInnerNode,
185
+ path: [path, ...(parsedInnerNode.path || [])]
186
+ };
187
+ }
188
+ } else {
189
+ const parsedCondition = parseNode(condition, pathPrefix);
190
+ if (parsedCondition) {
191
+ return {
192
+ ...parsedCondition,
193
+ path: [path, ...(parsedCondition.path || [])]
194
+ };
195
+ }
196
+ }
197
+ } else {
198
+ return {
199
+ path: [path],
200
+ op: '$eq',
201
+ value: condition
202
+ };
203
+ }
204
+ }
205
+
206
+ return null;
207
+ };
208
+
209
+
210
+ export const buildRootFromExpr = (query) => {
211
+ if (!query || typeof query !== 'object' || Array.isArray(query)) {
212
+ return { $and: [] };
213
+ }
214
+
215
+ const exprToParse = query.$expr || query;
216
+ const parsedRoot = parseNode(exprToParse);
217
+
218
+ if (!parsedRoot) return { $and: [] };
219
+ if (parsedRoot.$and || parsedRoot.$or) return parsedRoot;
220
+
221
+ return { $and: [parsedRoot] };
222
+ };
@@ -0,0 +1,101 @@
1
+ import {useQuery} from "react-query";
2
+ import {getObjectHash} from "data-primals-engine/core";
3
+ import {useAuthContext} from "../contexts/AuthContext.jsx";
4
+ import {getUserHash, getUserId} from "data-primals-engine/data";
5
+ import {useTranslation} from "react-i18next";
6
+ import {useEffect, useState} from "react";
7
+ import {useUI} from "../contexts/UIContext.jsx";
8
+ import {useNotificationContext} from "../NotificationProvider.jsx";
9
+
10
+ export const useData = (model, filter, options) => {
11
+ const { me } = useAuthContext();
12
+ const { i18n, t } = useTranslation();
13
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
14
+ return useQuery([options.queryKey || model, filter, me], () => {
15
+ return fetch('/api/data/search?limit='+(options.limit||1)+'&lang='+(options.lang||lang)+'&_user=' + encodeURIComponent(getUserId(me)) + '&model='+model, {
16
+ method: 'POST',
17
+ body: JSON.stringify({filter}),
18
+ headers: { "Content-Type": "application/json"}}
19
+ )
20
+ .then(e => e.json())
21
+ .then(e => e.data);
22
+ }, {...options, enabled: typeof(options.enabled) === "undefined" ? !!getUserId(me) : options.enabled });
23
+ }
24
+
25
+
26
+ export const useAlerts = () => {
27
+ // 2. Supprimer l'état local 'alerts', il sera géré par le UIContext
28
+ // const [alerts, setAlerts] = useState([]);
29
+ const [isConnected, setIsConnected] = useState(false);
30
+ const { me } = useAuthContext();
31
+ const { addNotification } = useNotificationContext(); // 3. Récupérer la fonction addNotification du contexte
32
+
33
+ useEffect(() => {
34
+ if (!me?.username || !addNotification) { // S'assurer que addNotification est disponible
35
+ if (isConnected) setIsConnected(false);
36
+ return;
37
+ }
38
+
39
+ console.log("Attempting to connect to SSE for alerts...");
40
+ const eventSource = new EventSource('/api/alerts/subscribe');
41
+
42
+ eventSource.onopen = () => {
43
+ console.log('SSE connection established for alerts.');
44
+ setIsConnected(true);
45
+ };
46
+
47
+ eventSource.onmessage = (event) => {
48
+ try {
49
+ const newAlert = JSON.parse(event.data);
50
+
51
+ if (newAlert.type === 'connection_established') {
52
+ console.log(`SSE Status: ${newAlert.message}`);
53
+ return;
54
+ }
55
+
56
+ // 4. Remplacer setAlerts par addNotification
57
+ // On crée un objet notification standard que votre système peut afficher.
58
+ if (newAlert.type === 'cron_alert') {
59
+ addNotification({
60
+ type: 'info', // ou 'success', 'warning' selon ce que votre système supporte
61
+ title: 'Alerte planifiée',
62
+ message: newAlert.message,
63
+ duration: 10000 // Durée d'affichage en ms (10 secondes)
64
+ });
65
+ }
66
+
67
+ // La notification native du navigateur peut rester si vous le souhaitez
68
+ /*if (Notification.permission === "granted") {
69
+ new Notification("Nouvelle Alerte", {
70
+ body: newAlert.message,
71
+ icon: "/logo.png"
72
+ });
73
+ }*/
74
+
75
+ } catch (error) {
76
+ console.error("Failed to parse SSE data:", error);
77
+ }
78
+ };
79
+
80
+ eventSource.onerror = (error) => {
81
+ console.error('SSE Error:', error);
82
+ setIsConnected(false);
83
+ eventSource.close();
84
+ };
85
+
86
+ return () => {
87
+ console.log("Closing SSE connection.");
88
+ eventSource.close();
89
+ setIsConnected(false);
90
+ };
91
+ }, [me?.username, addNotification]); // Ajouter addNotification aux dépendances
92
+
93
+ useEffect(() => {
94
+ /*if ("Notification" in window && Notification.permission !== "denied") {
95
+ Notification.requestPermission();
96
+ }*/
97
+ }, []);
98
+
99
+ // 5. Mettre à jour la valeur de retour. Le hook n'a plus besoin de retourner la liste des alertes.
100
+ return { isConnected };
101
+ };
@@ -0,0 +1,20 @@
1
+ // src/hooks/useDebounce.js
2
+ import { useState, useEffect } from 'react';
3
+
4
+ function useDebounce(value, delay) {
5
+ const [debouncedValue, setDebouncedValue] = useState(value);
6
+
7
+ useEffect(() => {
8
+ const handler = setTimeout(() => {
9
+ setDebouncedValue(value);
10
+ }, delay);
11
+
12
+ return () => {
13
+ clearTimeout(handler);
14
+ };
15
+ }, [value, delay]);
16
+
17
+ return debouncedValue;
18
+ }
19
+
20
+ export default useDebounce;
@@ -0,0 +1,59 @@
1
+ // C:/Dev/hackersonline-engine/client/src/hooks/useDragAndDrop.js
2
+ import { useState, useRef, useCallback } from 'react';
3
+
4
+ const useDragAndDrop = () => {
5
+ const [isDragging, setIsDragging] = useState(false);
6
+ const draggedItemRef = useRef(null); // Changed name for clarity
7
+ const [dragOverContainerId, setDragOverContainerId] = useState(null);
8
+ // dropPosition might not be needed if we simplify
9
+
10
+ const handleNodeDragStart = useCallback((nodeId) => {
11
+ setIsDragging(true);
12
+ draggedItemRef.current = nodeId;
13
+ console.log("useDragAndDrop: Dragging node with ID:", nodeId);
14
+ }, []);
15
+
16
+ const handleContainerDragEnter = useCallback((containerId) => {
17
+ setDragOverContainerId(containerId);
18
+ }, []);
19
+
20
+ const handleContainerDragLeave = useCallback(() => {
21
+ setDragOverContainerId(null);
22
+ }, []);
23
+
24
+ // This is for the draggable item's onDragEnd
25
+ const handleItemDragEnd = useCallback(() => {
26
+ setIsDragging(false);
27
+ // Do NOT reset draggedItemRef.current here.
28
+ // It's needed by the onDrop handler of the container.
29
+ console.log("useDragAndDrop: Item drag ended. Current draggedItemRef:", draggedItemRef.current);
30
+ }, []);
31
+
32
+ // This is for the droppable container's onDrop
33
+ const finalizeDropAndGetData = useCallback(() => {
34
+ const droppedItemId = draggedItemRef.current;
35
+
36
+ // Reset state after retrieving the ID
37
+ setIsDragging(false);
38
+ draggedItemRef.current = null;
39
+ setDragOverContainerId(null);
40
+ // setDropPosition(null); // if you were using it
41
+
42
+ console.log("useDragAndDrop: Finalized drop. Dropped ID:", droppedItemId);
43
+ return droppedItemId; // Return the ID of the item that was dropped
44
+ }, []);
45
+
46
+ return {
47
+ isDragging,
48
+ draggedItemRef, // Expose the ref object
49
+ dragOverContainerId,
50
+ // dropPosition,
51
+ handleNodeDragStart,
52
+ handleContainerDragEnter,
53
+ handleContainerDragLeave,
54
+ handleItemDragEnd, // For draggable's onDragEnd
55
+ finalizeDropAndGetData // For droppable's onDrop
56
+ };
57
+ };
58
+
59
+ export default useDragAndDrop;
@@ -0,0 +1,100 @@
1
+ import {useEffect, useMemo, useRef, useState} from "react";
2
+
3
+ function useLocalStorage(
4
+ key,
5
+ defaultValue,
6
+ options
7
+ ) {
8
+ const opts = useMemo(() => {
9
+ return {
10
+ serializer: JSON.stringify,
11
+ parser: JSON.parse,
12
+ logger: console.log,
13
+ syncData: true,
14
+ ...options,
15
+ };
16
+ }, [options]);
17
+
18
+ const { serializer, parser, logger, syncData } = opts;
19
+
20
+ const rawValueRef = useRef(null);
21
+
22
+ const [value, setValue] = useState(() => {
23
+ if (typeof window === "undefined") return defaultValue;
24
+
25
+ try {
26
+ rawValueRef.current = window.localStorage.getItem(key);
27
+ return rawValueRef.current
28
+ ? parser(rawValueRef.current)
29
+ : defaultValue;
30
+ } catch (e) {
31
+ logger(e);
32
+ return defaultValue;
33
+ }
34
+ });
35
+
36
+ useEffect(() => {
37
+ if (typeof window === "undefined") return;
38
+
39
+ const updateLocalStorage = () => {
40
+ // Browser ONLY dispatch storage events to other tabs, NOT current tab.
41
+ // We need to manually dispatch storage event for current tab
42
+ if (value !== undefined) {
43
+ const newValue = serializer(value);
44
+ const oldValue = rawValueRef.current;
45
+ rawValueRef.current = newValue;
46
+ window.localStorage.setItem(key, newValue);
47
+ window.dispatchEvent(
48
+ new StorageEvent("storage", {
49
+ storageArea: window.localStorage,
50
+ url: window.location.href,
51
+ key,
52
+ newValue,
53
+ oldValue,
54
+ })
55
+ );
56
+ } else {
57
+ window.localStorage.removeItem(key);
58
+ window.dispatchEvent(
59
+ new StorageEvent("storage", {
60
+ storageArea: window.localStorage,
61
+ url: window.location.href,
62
+ key,
63
+ })
64
+ );
65
+ }
66
+ };
67
+
68
+ try {
69
+ updateLocalStorage();
70
+ } catch (e) {
71
+ logger(e);
72
+ }
73
+ }, [value]);
74
+
75
+ useEffect(() => {
76
+ if (!syncData) return;
77
+
78
+ const handleStorageChange = (e) => {
79
+ if (e.key !== key || e.storageArea !== window.localStorage) return;
80
+
81
+ try {
82
+ if (e.newValue !== rawValueRef.current) {
83
+ rawValueRef.current = e.newValue;
84
+ setValue(e.newValue ? parser(e.newValue) : undefined);
85
+ }
86
+ } catch (e) {
87
+ logger(e);
88
+ }
89
+ };
90
+
91
+ if (typeof window === "undefined") return;
92
+
93
+ window.addEventListener("storage", handleStorageChange);
94
+ return () => window.removeEventListener("storage", handleStorageChange);
95
+ }, [key, syncData]);
96
+
97
+ return [value, setValue];
98
+ }
99
+
100
+ export default useLocalStorage;
@@ -0,0 +1,42 @@
1
+ import {useEffect, useRef} from "react";
2
+ import {useMutation} from "react-query";
3
+
4
+ export const useMount = (effect) => {
5
+ const mounted = useRef(false);
6
+
7
+ useEffect(() => {
8
+ if (mounted.current) {
9
+ return effect();
10
+ }
11
+
12
+ mounted.current = true;
13
+
14
+ return () => {};
15
+ }, [mounted, effect]);
16
+ };
17
+
18
+ export function useStrictModeMutation(options){
19
+
20
+ // `hasRun` tracks if the mutation has already been executed
21
+ const hasRun = useRef(false);
22
+
23
+ // Setting up the mutation with React Query
24
+ const mutation = useMutation(options);
25
+
26
+ // Conditional mutation function to prevent double execution
27
+ const strictModeMutate = (variables) => {
28
+ if (!hasRun.current) {
29
+ hasRun.current = true;
30
+ mutation.mutate(variables);
31
+ }
32
+ };
33
+
34
+ // Custom reset function to allow the mutation to be re-triggered
35
+ const reset = () => {
36
+ hasRun.current = false;
37
+ mutation.reset();
38
+ };
39
+
40
+ // Return the mutation with `strictModeMutate` instead of `mutate`
41
+ return { ...mutation, mutate: strictModeMutate, reset };
42
+ }