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,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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
248
|
+
const t = (field.itemsType || field.type);
|
|
249
|
+
if (t === 'code')
|
|
249
250
|
otherFields = ['maxlength', 'language', 'conditionBuilder'];
|
|
250
|
-
else if( ['string_t', 'string'].includes(
|
|
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">
|