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
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { useModelContext } from './contexts/ModelContext.jsx';
3
+ import FlexDataRenderer from './FlexDataRenderer.jsx';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ /**
7
+ * Renders a flexible data card, typically generated by the AI assistant.
8
+ * It displays a set of fields for a single data record.
9
+ * @param {object} props
10
+ * @param {object} props.config - The configuration for the view.
11
+ * @param {string} props.config.title - The title of the card.
12
+ * @param {string} props.config.model - The name of the data model.
13
+ * @param {string[]} props.config.fields - An array of field names to display.
14
+ * @param {object} props.config.data - The data record to display.
15
+ */
16
+ const FlexViewCard = ({ config }) => {
17
+ const { t } = useTranslation();
18
+ const { models } = useModelContext();
19
+ const { title, model: modelName, fields: fieldNames, data } = config;
20
+
21
+ const model = models.find(m => m.name === modelName);
22
+
23
+ if (!model || !data) {
24
+ return <div className="flex-view-card-error">{t('assistant.flexView.loadingError', 'Erreur de chargement de la vue.')}</div>;
25
+ }
26
+
27
+ const fieldsToDisplay = model.fields.filter(field => fieldNames.includes(field.name));
28
+
29
+ return (
30
+ <div className="flex-view-card">
31
+ {title && <h4>{title}</h4>}
32
+ <div className="data-card-content">
33
+ {fieldsToDisplay.map(fieldDef => (
34
+ <div key={fieldDef.name} className="data-field-row">
35
+ <strong className="field-label" title={fieldDef.name}>{t(`field_${model.name}_${fieldDef.name}`, fieldDef.name)}:</strong>
36
+ <div className="field-value"><FlexDataRenderer value={data[fieldDef.name]} fieldDefinition={fieldDef} data={data} /></div>
37
+ </div>
38
+ ))}
39
+ </div>
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export default FlexViewCard;
@@ -6,12 +6,20 @@ import {Trans, useTranslation} from "react-i18next";
6
6
  import "./HistoryDialog.scss";
7
7
  import {Tooltip} from "react-tooltip";
8
8
  import {CodeField} from "./Field.jsx";
9
+ import {Pagination} from "./Pagination.jsx";
10
+ import {useModelContext} from "./contexts/ModelContext.jsx";
9
11
 
10
12
  // Helper to render values in a friendly way for the summary list
11
- const renderSummaryValue = (value) => {
13
+ const renderSummaryValue = (value, field, t) => {
12
14
  if (value === undefined || value === null) {
13
15
  return <i className="value-empty">empty</i>;
14
16
  }
17
+ if (field?.type === 'string_t' && t) {
18
+ // For string_t, the stored value is the key. We display the translated value.
19
+ const key = (typeof value === 'object' && value?.key) ? value.key : String(value);
20
+ const displayValue = t(key, key);
21
+ return <span className="value-text" title={key}>{displayValue}</span>;
22
+ }
15
23
  if (typeof value === 'boolean') {
16
24
  return <code className="value-boolean">{value.toString()}</code>;
17
25
  }
@@ -31,16 +39,26 @@ const renderSummaryValue = (value) => {
31
39
  // New sub-component for rendering a single field change
32
40
  const ChangeDetail = ({ field, from, to }) => {
33
41
  const { t } = useTranslation();
42
+ const { selectedModel } = useModelContext();
43
+ const fieldObj = selectedModel?.fields?.find(f => f.name === field);
34
44
 
35
- const tt = typeof(to) === 'object' ? JSON.stringify(to,null,2):to;
36
- const f = typeof(from) === 'object' ? JSON.stringify(from,null,2):from;
45
+ const getTooltipText = (value, field) => {
46
+ if (field?.type === 'string_t') {
47
+ const key = (typeof value === 'object' && value?.key) ? value.key : String(value);
48
+ return t(key, key); // Affiche la valeur traduite dans la tooltip
49
+ }
50
+ return (typeof value === 'object' && value !== null) ? JSON.stringify(value, null, 2) : String(value);
51
+ };
52
+
53
+ const tt = getTooltipText(to, fieldObj);
54
+ const f = getTooltipText(from, fieldObj);
37
55
  return (
38
56
  <div className="change-detail">
39
57
  <strong className="change-field-name" title={field}>{field}</strong>
40
58
  <div className="change-values">
41
- <div className="value-from" data-tooltip-id={"changeTooltip"} data-tooltip-content={f}>{renderSummaryValue(from)}</div>
59
+ <div className="value-from" data-tooltip-id={"changeTooltip"} data-tooltip-content={f}>{renderSummaryValue(from, fieldObj, t)}</div>
42
60
  <div className="change-arrow">→</div>
43
- <div className="value-to" data-tooltip-id={"changeTooltip"} data-tooltip-content={tt}>{renderSummaryValue(to)}</div>
61
+ <div className="value-to" data-tooltip-id={"changeTooltip"} data-tooltip-content={tt}>{renderSummaryValue(to, fieldObj, t)}</div>
44
62
  </div>
45
63
  </div>
46
64
  );
@@ -49,6 +67,7 @@ const ChangeDetail = ({ field, from, to }) => {
49
67
  // Sub-component for displaying a single history entry
50
68
  const HistoryEntry = ({ entry, onPreview, isSelected }) => {
51
69
  const { t } = useTranslation();
70
+ const { selectedModel } = useModelContext();
52
71
  // The backend has transformed the data for us.
53
72
  // _id: history doc id, _v: version, _op: operation type (i,u,d), _rid: original record id
54
73
  const { _id, _v, _op, _updatedAt, _user, ...data } = entry; // ...data collects the remaining fields
@@ -83,12 +102,13 @@ const HistoryEntry = ({ entry, onPreview, isSelected }) => {
83
102
  }
84
103
  return (
85
104
  <div className="snapshot-list">
86
- {snapshotEntries.map(([key, value]) => (
87
- <div className="snapshot-item" key={key}>
105
+ {snapshotEntries.map(([key, value]) => {
106
+ const fieldObj = selectedModel?.fields?.find(f => f.name === key);
107
+ return (<div className="snapshot-item" key={key}>
88
108
  <strong className="snapshot-key" title={key}>{key}:</strong>
89
- <span className="snapshot-value">{renderSummaryValue(value)}</span>
90
- </div>
91
- ))}
109
+ <span className="snapshot-value">{renderSummaryValue(value, fieldObj, t)}</span>
110
+ </div>);
111
+ })}
92
112
  </div>
93
113
  );
94
114
  };
@@ -118,6 +138,11 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
118
138
  const [previewData, setPreviewData] = useState(null);
119
139
  const [previewLoading, setPreviewLoading] = useState(false);
120
140
  const [selectedVersion, setSelectedVersion] = useState(null);
141
+ const [page, setPage] = useState(1);
142
+ const [totalCount, setTotalCount] = useState(0);
143
+ const [startDate, setStartDate] = useState('');
144
+ const [endDate, setEndDate] = useState('');
145
+ const elementsPerPage = 10;
121
146
  const { t, i18n } = useTranslation();
122
147
 
123
148
  const fetchHistory = useCallback(async () => {
@@ -125,10 +150,14 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
125
150
  setLoading(true);
126
151
  setError(null);
127
152
  try {
128
- const query = await fetch(`/api/data/history/${modelName}/${recordId}`);
153
+ const params = new URLSearchParams({ page, limit: elementsPerPage, lang: i18n.language });
154
+ if (startDate) params.append('startDate', startDate);
155
+ if (endDate) params.append('endDate', endDate);
156
+ const query = await fetch(`/api/data/history/${modelName}/${recordId}?${params.toString()}`);
129
157
  const response = await query.json();
130
158
  if (response.success) {
131
159
  setHistory(response.data);
160
+ setTotalCount(response.count || 0);
132
161
  } else {
133
162
  setError(response.error || i18n.t("history.error", "Could not load history."));
134
163
  }
@@ -138,7 +167,12 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
138
167
  } finally {
139
168
  setLoading(false);
140
169
  }
141
- }, [modelName, recordId, i18n]);
170
+ }, [modelName, recordId, i18n.language, page, elementsPerPage, startDate, endDate]);
171
+
172
+ // Réinitialise la page à 1 lorsque les filtres de date changent
173
+ useEffect(() => {
174
+ setPage(1);
175
+ }, [startDate, endDate]);
142
176
 
143
177
  useEffect(() => {
144
178
  fetchHistory();
@@ -207,8 +241,21 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
207
241
  <div className="history-dialog-content-split">
208
242
  <div className="history-list-container">
209
243
  {loading && <div><Trans i18nKey={"history.loading"}>Chargement de l'historique..</Trans></div>}
244
+ <div className="history-filters">
245
+ <div className="date-filter">
246
+ <label htmlFor="startDate"><Trans i18nKey="history.startDate">From</Trans></label>
247
+ <input type="date" id="startDate" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
248
+ </div>
249
+ <div className="date-filter">
250
+ <label htmlFor="endDate"><Trans i18nKey="history.endDate">To</Trans></label>
251
+ <input type="date" id="endDate" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
252
+ </div>
253
+ <Button className="btn-clear-filters" onClick={() => { setStartDate(''); setEndDate(''); }} title={t('history.clearDates', 'Clear date filters')}>
254
+ <Trans i18nKey="history.clear">Clear</Trans>
255
+ </Button>
256
+ </div>
210
257
  {error && <div className="error-message">{error}</div>}
211
- {!loading && !error && (
258
+ {!loading && !error && (<>
212
259
  <div className="history-list">
213
260
  {history.length > 0 ? (
214
261
  history.map(entry =>
@@ -222,7 +269,15 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
222
269
  <div><Trans i18nKey={"history.noHistory"}>Aucun historique trouvé pour cet enregistrement.</Trans></div>
223
270
  )}
224
271
  </div>
225
- )}
272
+ {totalCount > elementsPerPage && <Pagination
273
+ totalCount={totalCount}
274
+ page={page}
275
+ setPage={setPage}
276
+ elementsPerPage={elementsPerPage}
277
+ visibleItemsCount={5}
278
+ hasPreviousNext={true}
279
+ />}
280
+ </>)}
226
281
  </div>
227
282
  <div className="history-preview-container">
228
283
  <div className="preview-header">
@@ -0,0 +1,91 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Dialog } from './Dialog.jsx';
4
+ import {TextField, SelectField, NumberField, CodeField, ModelField} from './Field.jsx';
5
+ import Button from './Button.jsx';
6
+ import { FaSave, FaTimes } from 'react-icons/fa';
7
+ import './HtmlViewBuilderModal.scss';
8
+
9
+ const HtmlViewBuilderModal = ({ isOpen, onClose, onSave, models, initialConfig }) => {
10
+ const { t } = useTranslation();
11
+ const [config, setConfig] = useState({});
12
+ const [jsonError, setJsonError] = useState('');
13
+
14
+ useEffect(() => {
15
+ if (isOpen) {
16
+ setConfig(initialConfig || {
17
+ title: '',
18
+ model: '',
19
+ limit: 10,
20
+ filter: {},
21
+ template: '',
22
+ css: '',
23
+ ...initialConfig
24
+ });
25
+ setJsonError('');
26
+ }
27
+ }, [isOpen, initialConfig]);
28
+
29
+ const modelOptions = useMemo(() => {
30
+ return (models || []).map(m => ({ label: m.name, value: m.name }));
31
+ }, [models]);
32
+
33
+ const handleChange = (e) => {
34
+ const { name, value } = e.target || e;
35
+ setConfig(prev => ({ ...prev, [name]: value }));
36
+ };
37
+
38
+ const handleCodeChange = (name, value) => {
39
+ setConfig(prev => ({ ...prev, [name]: value }));
40
+ };
41
+
42
+ const handleFilterChange = (codeValue) => {
43
+ try {
44
+ const parsed = JSON.parse(codeValue);
45
+ setConfig(prev => ({ ...prev, filter: parsed }));
46
+ setJsonError('');
47
+ } catch (e) {
48
+ setJsonError(t('dashboards.invalidJson', 'JSON invalide'));
49
+ }
50
+ };
51
+
52
+ const handleSave = () => {
53
+ if (!config.model || !config.template) {
54
+ console.error("Model and template are required.");
55
+ return;
56
+ }
57
+ if (jsonError) return;
58
+ onSave(config);
59
+ };
60
+
61
+ if (!isOpen) return null;
62
+
63
+ return (
64
+ <Dialog
65
+ isModal={true}
66
+ isClosable={true}
67
+ onClose={onClose}
68
+ title={initialConfig?.id ? t('dashboards.editHtmlViewTitle', 'Modifier la Vue HTML') : t('dashboards.addHtmlViewTitle', 'Ajouter une Vue HTML')}
69
+ className="html-view-builder-modal"
70
+ >
71
+ <div className="form-grid">
72
+ <TextField name="title" label={t('dashboards.widgetTitle', 'Titre du widget')} value={config.title || ''} onChange={handleChange} />
73
+ <ModelField name="model" label={t('model', 'Modèle')} value={config.model || ''} onChange={(option) => handleChange({ name: 'model', value: option?.value })} required />
74
+ <NumberField name="limit" label={t('dashboards.dataLimit', 'Limite de données')} value={config.limit ?? 10} onChange={handleChange} min={1} max={100} />
75
+ <CodeField name="filter" label={t('filter', 'Filtre (JSON)')} value={typeof config.filter === 'string' ? config.filter : JSON.stringify(config.filter || {}, null, 2)} language="json" onChange={(e) => handleFilterChange(e.value)} error={jsonError} />
76
+ <CodeField name="template" label={t('dashboards.htmlTemplate', 'Template HTML')} value={config.template || ''} language="html" onChange={(e) => handleCodeChange('template', e.value)} required height="200px" />
77
+ <CodeField name="css" label={t('dashboards.cssStyles', 'Styles CSS')} value={config.css || ''} language="css" onChange={(e) => handleCodeChange('css', e.value)} height="150px" />
78
+ </div>
79
+ <div className="flex actions right">
80
+ <Button onClick={onClose} className="btn-secondary">
81
+ <FaTimes /> {t('btns.cancel', 'Annuler')}
82
+ </Button>
83
+ <Button onClick={handleSave} className="btn-primary" disabled={!!jsonError}>
84
+ <FaSave /> {t('btns.save', 'Enregistrer')}
85
+ </Button>
86
+ </div>
87
+ </Dialog>
88
+ );
89
+ };
90
+
91
+ export default HtmlViewBuilderModal;
@@ -0,0 +1,18 @@
1
+ .html-view-builder-modal {
2
+ .form-grid {
3
+ display: grid;
4
+ grid-template-columns: 1fr;
5
+ gap: 1rem;
6
+ padding-bottom: 1rem;
7
+
8
+ .code-editor-container {
9
+ grid-column: 1 / -1;
10
+ }
11
+ }
12
+
13
+ .actions {
14
+ margin-top: 1rem;
15
+ padding-top: 1rem;
16
+ border-top: 1px solid var(--border-color);
17
+ }
18
+ }
@@ -0,0 +1,44 @@
1
+ import React, {useEffect, useMemo} from 'react';
2
+ import './HtmlViewCard.scss';
3
+ import {renderHtmlTemplate} from "./DashboardHtmlViewItem.jsx";
4
+
5
+ /**
6
+ * Affiche une vue HTML personnalisée, généralement générée par l'assistant IA.
7
+ * @param {object} props
8
+ * @param {object} props.config - La configuration de la vue.
9
+ * @param {string} props.config.title - Le titre de la carte.
10
+ * @param {string} props.config.template - La chaîne de template HTML Handlebars.
11
+ * @param {Array<object>} props.config.data - Le tableau d'enregistrements de données à afficher.
12
+ */
13
+ const HtmlViewCard = ({ config }) => {
14
+ const { title, template, data, css } = config;
15
+
16
+ // Génère un ID unique et stable pour ce composant
17
+ const containerId = useMemo(() => `html-view-${Math.random().toString(36).substring(2, 9)}`, []);
18
+
19
+ // Effet pour injecter et nettoyer le CSS
20
+ useEffect(() => {
21
+ if (!css) return;
22
+
23
+ const styleElement = document.createElement('style');
24
+ // Remplace le placeholder par notre ID unique pour scoper le CSS
25
+ styleElement.innerHTML = css.replace(/\{\{containerId\}\}/g, containerId);
26
+ document.head.appendChild(styleElement);
27
+
28
+ // Fonction de nettoyage : retire le style quand le composant est démonté
29
+ return () => {
30
+ document.head.removeChild(styleElement);
31
+ };
32
+ }, [css, containerId]);
33
+
34
+ const renderedHtml = renderHtmlTemplate(template, data);
35
+
36
+ return (
37
+ <div id={containerId} className="html-view-card">
38
+ {title && <h4>{title}</h4>}
39
+ <div className="html-view-content" dangerouslySetInnerHTML={{ __html: renderedHtml }} />
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export default HtmlViewCard;
@@ -0,0 +1,35 @@
1
+ .html-view-card {
2
+ background-color: var(--bg-color-dark);
3
+ border: 1px solid var(--border-color);
4
+ border-radius: 8px;
5
+ margin-top: 12px;
6
+ font-size: 0.9rem;
7
+
8
+ h4 {
9
+ margin-top: 0;
10
+ margin-bottom: 12px;
11
+ color: var(--text-color-light);
12
+ border-bottom: 1px solid var(--border-color-light);
13
+ padding-bottom: 8px;
14
+ }
15
+
16
+ .html-view-content {
17
+ ul, ol {
18
+ padding-left: 20px;
19
+ margin-top: 8px;
20
+ margin-bottom: 8px;
21
+ }
22
+ li { margin-bottom: 4px; }
23
+ a {
24
+ color: var(--primary-color);
25
+ text-decoration: none;
26
+ &:hover { text-decoration: underline; }
27
+ }
28
+ h1, h2, h3 {
29
+ margin-top: 1em;
30
+ margin-bottom: 0.5em;
31
+ color: var(--text-color);
32
+ }
33
+ p { line-height: 1.5; }
34
+ }
35
+ }
@@ -14,7 +14,6 @@ const KanbanCard = ({ card, model, subItemsField }) => {
14
14
  // Logique du Drag & Drop natif pour la carte
15
15
  const lang = (tr.i18n.resolvedLanguage || tr.i18n.language).split(/[-_]/)?.[0];
16
16
 
17
- console.log(subItems)
18
17
  const getfield = (model, data) => {
19
18
  const v = getDataAsString(model, data, tr, models);
20
19
  if(!v){
@@ -26,7 +25,7 @@ const KanbanCard = ({ card, model, subItemsField }) => {
26
25
  <strong>guid</strong> : ${it.guid}<br />
27
26
  <strong>type</strong> : ${it.mimeType}<br />
28
27
  <strong>size</strong> : ${it.size} bytes<br />
29
- <strong>timestamp</strong> : ${it.timestamp ? new Date(it.timestamp).toLocaleString(lang) : ''}
28
+ <strong>timestamp</strong> : ${it.createdAt ? new Date(it.createdAt).toLocaleString(lang) : ''}
30
29
  `;
31
30
  return <a key={it.guid} href={`/resources/${it.guid}`} target="_blank"
32
31
  data-tooltip-id={"tooltipFile"} data-tooltip-html={r}
@@ -225,7 +225,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
225
225
  delete field['_isNewField'];
226
226
  let otherFields = [];
227
227
  // Check for specific field types
228
- switch (field.type) {
228
+ switch ((field.type)) {
229
229
  case 'relation':
230
230
  otherFields = ['relation', 'multiple', 'relationFilter'];
231
231
  break;
@@ -245,10 +245,11 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
245
245
  case 'phone':
246
246
  case 'password':
247
247
  case 'code':
248
- if (field.type === 'code')
248
+ const t = (field.itemsType || field.type);
249
+ if (t === 'code')
249
250
  otherFields = ['maxlength', 'language', 'conditionBuilder'];
250
- else if( ['string_t', 'string'].includes(field.type))
251
- otherFields = ['maxlength', 'multiline'];
251
+ else if( ['string_t', 'string'].includes(t))
252
+ otherFields = ['maxlength', 'multiline', 'mask', 'replacement'];
252
253
  else
253
254
  otherFields = ['maxlength'];
254
255
  break;
@@ -1,4 +1,4 @@
1
- import React, {useState} from "react";
1
+ import React, {useEffect, useState} from "react";
2
2
  import {useModelContext} from "./contexts/ModelContext.jsx";
3
3
  import {useAuthContext} from "./contexts/AuthContext.jsx";
4
4
  import {isLocalUser} from "../../src/data.js";
@@ -43,6 +43,13 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
43
43
  const [showMore, setMoreVisible] = useState(false);
44
44
  const hint = (description) => t(description, '') && <div className="hint-icon"><FaCircleInfo data-tooltip-id={`tooltipHint`} data-tooltip-content={description} /></div>
45
45
 
46
+ useEffect(() => {
47
+ // S'assure que replacement est défini si un masque existe, avec une valeur par défaut correcte.
48
+ // La chaîne "\\d" en JS devient "\d" dans l'objet, ce qui est correct pour new RegExp().
49
+ if( field.mask && !field.replacement )
50
+ field.replacement = { "_": "\\d" };
51
+ }, [field]);
52
+
46
53
  return (
47
54
  <div className="field-edit">
48
55
 
@@ -288,6 +295,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
288
295
  />
289
296
  </div>
290
297
  </div>
298
+
291
299
  </>
292
300
  )}
293
301
 
@@ -577,8 +585,47 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
577
585
  </>)}
578
586
  </div>)}
579
587
 
588
+ {["string_t", "string"].includes(field.itemsType || field.type) && (
589
+ <>
590
+
591
+ <div className={"flex flex-no-wrap field-bg"}>
592
+ {hint('modelcreator.mask.hint')}
593
+ <div className={"flex-1"}>
594
+ <TextField
595
+ disabled={modelLocked || (isLocalUser(me) && field.locked)}
596
+ label={<Trans i18nKey={"modelcreator.mask"}>Masque de saisie</Trans>}
597
+ className={"flex-1"}
598
+ value={field.mask}
599
+ placeholder={t('modelcreator.field.mask.ph', "mask_")}
600
+ onChange={(e) => {
601
+ const newFields = [...fields];
602
+ newFields[index].mask = e.target.value;
603
+ setFields(newFields);
604
+ }}
605
+ />
606
+ </div>
607
+ </div>
608
+ <div className={"flex flex-no-wrap field-bg"}>
609
+ {hint('modelcreator.replacement.hint')}
610
+ <div className={"flex-1"}>
611
+ <CodeField
612
+ name="replacement"
613
+ disabled={modelLocked || (isLocalUser(me) && field.locked)}
614
+ label={<Trans i18nKey={"modelcreator.replacement"}>Règles de remplacement (JSON)</Trans>}
615
+ language="json"
616
+ value={field.replacement ? JSON.stringify(field.replacement, null, 2) : ''}
617
+ onChange={(e) => {
618
+ const newFields = [...fields];
619
+ newFields[index].replacement = e.value;
620
+ setFields(newFields);
621
+ }}
622
+ />
623
+ </div>
624
+ </div>
625
+ </>
626
+ )}
580
627
 
581
- {field.type === 'number' && (
628
+ {(field.itemsType || field.type) === 'number' && (
582
629
  <>
583
630
  <div
584
631
  className="flex flex-no-wrap mg-item">{hint('modelcreator.min.hint')}
@@ -685,7 +732,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
685
732
  </div>
686
733
 
687
734
 
688
- {field.type=== 'number'&&(
735
+ {(field.itemsType || field.type)=== 'number'&&(
689
736
  <div className="flex flex-no-wrap">
690
737
  {hint('modelcreator.gauge.hint')}
691
738
  <div className="checkbox-label flex flex-1">
@@ -736,7 +783,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
736
783
 
737
784
  </div>
738
785
  )}
739
- {field.type === 'code' && (<>
786
+ {(field.itemsType || field.type) === 'code' && (<>
740
787
  <div className="flex">
741
788
  {hint('modelcreator.language.hint')}
742
789
  <label className="checkbox-label flex flex-1">